Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0bf52a5d55 | |||
| e8d37df18e | |||
| 61e105def2 | |||
| fd753291fe | |||
| 35e98b9ab1 | |||
| 0915529af0 | |||
| 03d319ebf6 | |||
| d29075e9b4 | |||
| 10097f3003 | |||
| c352a29b4e | |||
| 7c40ae9309 |
20
components.d.ts
vendored
Normal file
20
components.d.ts
vendored
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
// @ts-nocheck
|
||||||
|
// Generated by unplugin-vue-components
|
||||||
|
// Read more: https://github.com/vuejs/core/pull/3399
|
||||||
|
// biome-ignore lint: disable
|
||||||
|
export {}
|
||||||
|
|
||||||
|
/* prettier-ignore */
|
||||||
|
declare module 'vue' {
|
||||||
|
export interface GlobalComponents {
|
||||||
|
Chart: typeof import('./src/components/chart/index.vue')['default']
|
||||||
|
NDialogProvider: typeof import('naive-ui')['NDialogProvider']
|
||||||
|
NLoadingBarProvider: typeof import('naive-ui')['NLoadingBarProvider']
|
||||||
|
NMessageProvider: typeof import('naive-ui')['NMessageProvider']
|
||||||
|
NModalProvider: typeof import('naive-ui')['NModalProvider']
|
||||||
|
ProviderHelper: typeof import('./src/components/ProviderHelper.vue')['default']
|
||||||
|
RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
|
RouterView: typeof import('vue-router')['RouterView']
|
||||||
|
}
|
||||||
|
}
|
||||||
2367
package-lock.json
generated
2367
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
19
package.json
19
package.json
@@ -15,19 +15,22 @@
|
|||||||
"echarts": "^5.4.3",
|
"echarts": "^5.4.3",
|
||||||
"echarts-liquidfill": "^3.1.0",
|
"echarts-liquidfill": "^3.1.0",
|
||||||
"jsencrypt": "^3.5.4",
|
"jsencrypt": "^3.5.4",
|
||||||
"naive-ui": "^2.44.1",
|
|
||||||
"ol": "^7.4.0",
|
"ol": "^7.4.0",
|
||||||
"pinia": "^3.0.4",
|
"pinia": "^3.0.4",
|
||||||
"prettier": "^3.0.0",
|
"prettier": "^3.0.0",
|
||||||
"vue": "^3.2.47",
|
"vue": "^3.5.39",
|
||||||
"vue-router": "^4.6.4"
|
"vue-router": "^5.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vitejs/plugin-vue": "^4.1.0",
|
"@vicons/fluent": "^0.12.0",
|
||||||
"@vitejs/plugin-vue-jsx": "^1.3.3",
|
"@vitejs/plugin-vue": "^6.0.8",
|
||||||
|
"@vitejs/plugin-vue-jsx": "^5.1.6",
|
||||||
|
"@vue/compiler-sfc": "^3.5.39",
|
||||||
"less": "^4.1.3",
|
"less": "^4.1.3",
|
||||||
"typescript": "^4.9.3",
|
"naive-ui": "^2.44.1",
|
||||||
"vite": "^4.2.0",
|
"typescript": "^5.9.3",
|
||||||
"vue-tsc": "^1.2.0"
|
"unplugin-vue-components": "^28.7.0",
|
||||||
|
"vite": "8.1.4",
|
||||||
|
"vue-tsc": "^3.3.9"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
public/3156245b41302edd52a992df20d51aea.png
Normal file
BIN
public/3156245b41302edd52a992df20d51aea.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.1 MiB |
BIN
public/image.png
Normal file
BIN
public/image.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 MiB |
BIN
public/room.png
BIN
public/room.png
Binary file not shown.
|
Before Width: | Height: | Size: 999 B |
13
src/App.vue
13
src/App.vue
@@ -43,7 +43,18 @@ onBeforeUnmount(() => {
|
|||||||
<template>
|
<template>
|
||||||
<div class="screen-adapter">
|
<div class="screen-adapter">
|
||||||
<div class="screen-adapter__inner" :style="adapterStyle">
|
<div class="screen-adapter__inner" :style="adapterStyle">
|
||||||
<RouterView />
|
<n-loading-bar-provider>
|
||||||
|
<n-modal-provider>
|
||||||
|
<n-dialog-provider>
|
||||||
|
<n-message-provider>
|
||||||
|
<n-message-provider>
|
||||||
|
<provider-helper />
|
||||||
|
<router-view />
|
||||||
|
</n-message-provider>
|
||||||
|
</n-message-provider>
|
||||||
|
</n-dialog-provider>
|
||||||
|
</n-modal-provider>
|
||||||
|
</n-loading-bar-provider>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,66 +1,84 @@
|
|||||||
import request from "@/utils/request";
|
import request from "@/utils/request";
|
||||||
|
import type { ApiResult, OrderItem, PageParam, PageResult } from "@/types/api";
|
||||||
|
import type { RosCar } from "./rosCar";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 告警列表查询条件,对应后端 com.haizhiyustc.ros.dto.RosInspectionRecordLineImageQueryDTO
|
||||||
|
*/
|
||||||
export interface RosInspectionRecordLineImageQueryDTO {
|
export interface RosInspectionRecordLineImageQueryDTO {
|
||||||
|
/** 识别类型 */
|
||||||
recognizeType?: number | string;
|
recognizeType?: number | string;
|
||||||
|
/** 开始时间 */
|
||||||
startTime?: string;
|
startTime?: string;
|
||||||
|
/** 结束时间 */
|
||||||
endTime?: string;
|
endTime?: string;
|
||||||
|
/** 是否预警 */
|
||||||
warning?: boolean;
|
warning?: boolean;
|
||||||
|
/** 关键字 */
|
||||||
keyword?: string;
|
keyword?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OrderItem {
|
|
||||||
property: string;
|
|
||||||
tableIndex: number;
|
|
||||||
asc?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PageParam<T> {
|
|
||||||
page?: number;
|
|
||||||
limit?: number;
|
|
||||||
orderList?: OrderItem[];
|
|
||||||
query?: T;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type RosInspectionRecordLineImagePageQuery =
|
export type RosInspectionRecordLineImagePageQuery =
|
||||||
Partial<RosInspectionRecordLineImage> & {
|
Partial<RosInspectionRecordLineImage> & {
|
||||||
keyword?: string;
|
keyword?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface RosInspectionRecordLineImageCar {
|
/**
|
||||||
id?: string;
|
* 巡检路线抓拍图片识别记录,对应后端 com.haizhiyustc.ros.entity.RosInspectionRecordLineImage
|
||||||
deviceKey?: string;
|
* recognitionStatus: 0待执行 1执行中 2执行成功 3执行失败
|
||||||
deviceNo?: string;
|
*/
|
||||||
name?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RosInspectionRecordLineImage {
|
export interface RosInspectionRecordLineImage {
|
||||||
|
/** 巡检记录id */
|
||||||
recordId: string;
|
recordId: string;
|
||||||
|
/** 巡检设备id快照 */
|
||||||
carId?: string;
|
carId?: string;
|
||||||
car?: RosInspectionRecordLineImageCar;
|
/** 关联的小车信息 */
|
||||||
|
car?: RosCar;
|
||||||
|
/** 抓拍动作id */
|
||||||
recordActionId: string;
|
recordActionId: string;
|
||||||
|
/** 所属任务id */
|
||||||
taskId?: string;
|
taskId?: string;
|
||||||
|
/** 识别类型 */
|
||||||
recognizeType?: number;
|
recognizeType?: number;
|
||||||
|
/** 图片访问地址 */
|
||||||
imageUrl?: string;
|
imageUrl?: string;
|
||||||
|
/** 抓拍时间 */
|
||||||
captureTime?: string;
|
captureTime?: string;
|
||||||
|
/** 抓拍时的小车位姿x */
|
||||||
poseX?: number;
|
poseX?: number;
|
||||||
|
/** 抓拍时的小车位姿y */
|
||||||
poseY?: number;
|
poseY?: number;
|
||||||
|
/** 抓拍时的小车朝向 */
|
||||||
poseYaw?: number;
|
poseYaw?: number;
|
||||||
|
/** 识别状态:0待执行 1执行中 2执行成功 3执行失败 */
|
||||||
recognitionStatus?: number;
|
recognitionStatus?: number;
|
||||||
|
/** AI识别提示词 */
|
||||||
prompt?: string;
|
prompt?: string;
|
||||||
|
/** AI识别结果 */
|
||||||
recognitionResult?: string;
|
recognitionResult?: string;
|
||||||
|
/** 是否预警 */
|
||||||
warning?: boolean;
|
warning?: boolean;
|
||||||
|
/** 预警值 */
|
||||||
warningValue?: string;
|
warningValue?: string;
|
||||||
|
/** 失败原因 */
|
||||||
failReason?: string;
|
failReason?: string;
|
||||||
|
/** 开始识别时间 */
|
||||||
startTime?: string;
|
startTime?: string;
|
||||||
|
/** 结束识别时间 */
|
||||||
endTime?: string;
|
endTime?: string;
|
||||||
|
/** 创建时间 */
|
||||||
createTime?: string;
|
createTime?: string;
|
||||||
|
/** 更新时间 */
|
||||||
updateTime?: string;
|
updateTime?: string;
|
||||||
|
/** 主键id */
|
||||||
id?: string;
|
id?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 查询近期告警列表
|
// 查询近期告警列表
|
||||||
|
// 返回告警列表,对应后端 warnList 接口
|
||||||
|
// 返回: ApiResult<List<RosInspectionRecordLineImage>>
|
||||||
export function getRecentAlarmList(data: RosInspectionRecordLineImageQueryDTO) {
|
export function getRecentAlarmList(data: RosInspectionRecordLineImageQueryDTO) {
|
||||||
return request({
|
return request<ApiResult<RosInspectionRecordLineImage[]>>({
|
||||||
url: "/admin/rosInspectionRecordLineImage/warnList",
|
url: "/admin/rosInspectionRecordLineImage/warnList",
|
||||||
data,
|
data,
|
||||||
});
|
});
|
||||||
@@ -68,7 +86,7 @@ export function getRecentAlarmList(data: RosInspectionRecordLineImageQueryDTO) {
|
|||||||
|
|
||||||
// 分页查询告警列表
|
// 分页查询告警列表
|
||||||
export function getAlarmPage(data: PageParam<RosInspectionRecordLineImagePageQuery>) {
|
export function getAlarmPage(data: PageParam<RosInspectionRecordLineImagePageQuery>) {
|
||||||
return request({
|
return request<ApiResult<PageResult<RosInspectionRecordLineImage>>>({
|
||||||
url: "/admin/rosInspectionRecordLineImage/list",
|
url: "/admin/rosInspectionRecordLineImage/list",
|
||||||
method: "POST",
|
method: "POST",
|
||||||
data,
|
data,
|
||||||
|
|||||||
@@ -1,8 +1,230 @@
|
|||||||
import request from "@/utils/request";
|
import request from "@/utils/request";
|
||||||
|
import type { ApiResult, PageParam, PageResult } from "@/types/api";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 升降柱状态,对应后端 com.haizhiyustc.ros.dto.RosLiftDTO
|
||||||
|
*/
|
||||||
|
export interface RosLift {
|
||||||
|
/** 升降柱接口是否调用成功 */
|
||||||
|
liftSuccess?: boolean;
|
||||||
|
/** 升降柱接口返回码 */
|
||||||
|
liftCode?: number;
|
||||||
|
/** 升降柱接口返回消息 */
|
||||||
|
liftMessage?: string;
|
||||||
|
/** 升降柱设备状态码 */
|
||||||
|
liftDeviceStatusCode?: string;
|
||||||
|
/** 升降柱设备状态描述 */
|
||||||
|
liftDeviceStatusDescription?: string;
|
||||||
|
/** 升降柱错误码 */
|
||||||
|
liftErrorCode?: string;
|
||||||
|
/** 升降柱错误描述 */
|
||||||
|
liftErrorDescription?: string;
|
||||||
|
/** 升降柱高度 */
|
||||||
|
liftHeight?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ROS 小车,对应后端 com.haizhiyustc.ros.entity.RosCar
|
||||||
|
* status: 0空闲 1巡检 2离线 3充电中
|
||||||
|
*/
|
||||||
|
export interface RosCar {
|
||||||
|
/** 主键id */
|
||||||
|
id?: string;
|
||||||
|
/** 设备唯一标识 */
|
||||||
|
deviceKey?: string;
|
||||||
|
/** 设备编号 */
|
||||||
|
deviceNo?: string;
|
||||||
|
/** 小车信号 */
|
||||||
|
signal?: string;
|
||||||
|
/** 小车名称 */
|
||||||
|
name?: string;
|
||||||
|
/** 电量百分比 */
|
||||||
|
batteryLevel?: number;
|
||||||
|
/** 电压 */
|
||||||
|
voltage?: number;
|
||||||
|
/** 当前状态:0空闲 1巡检 2离线 3充电中 */
|
||||||
|
status?: number;
|
||||||
|
/** 所属项目名称 */
|
||||||
|
projectName?: string;
|
||||||
|
/** 当前 X 坐标 */
|
||||||
|
poseX?: number;
|
||||||
|
/** 当前 Y 坐标 */
|
||||||
|
poseY?: number;
|
||||||
|
/** 当前朝向 */
|
||||||
|
poseYaw?: number;
|
||||||
|
/** 相机 WebRTC 地址 */
|
||||||
|
cameraWebrtcUrl?: string;
|
||||||
|
/** 相机编号 */
|
||||||
|
cameraNo?: string;
|
||||||
|
/** 红外相机 WebRTC 地址 */
|
||||||
|
cameraWebrtcUrl2?: string;
|
||||||
|
/** 红外相机编号 */
|
||||||
|
cameraNo2?: string;
|
||||||
|
/** 云台预制点 */
|
||||||
|
panTiltPreset?: number;
|
||||||
|
/** 地图 PGM 文件 URL 地址 */
|
||||||
|
mapPgmUrl?: string;
|
||||||
|
/** 地图 YAML 文件 URL 地址 */
|
||||||
|
mapYamlUrl?: string;
|
||||||
|
/** 升降柱高度 */
|
||||||
|
liftHeight?: number;
|
||||||
|
/** 升降柱状态 */
|
||||||
|
lift?: RosLift;
|
||||||
|
/** 创建时间 */
|
||||||
|
createTime?: string;
|
||||||
|
/** 更新时间 */
|
||||||
|
updateTime?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 巡检记录,对应后端 com.haizhiyustc.ros.entity.RosInspectionRecord
|
||||||
|
* status: 0执行中 1执行完成 2执行失败 3已停止
|
||||||
|
*/
|
||||||
|
export interface RosInspectionRecord {
|
||||||
|
/** 主键id */
|
||||||
|
id?: string;
|
||||||
|
/** 所属巡检任务id */
|
||||||
|
taskId?: string;
|
||||||
|
/** 巡检设备id */
|
||||||
|
carId?: string;
|
||||||
|
/** 巡检说明快照 */
|
||||||
|
remark?: string;
|
||||||
|
/** 全局限速快照 */
|
||||||
|
maxVelocity?: number;
|
||||||
|
/** 插值密度快照(米/点) */
|
||||||
|
interpolationDensity?: number;
|
||||||
|
/** 巡检地图名快照 */
|
||||||
|
mapName?: string;
|
||||||
|
/** 巡检轮数快照 */
|
||||||
|
roundCount?: number;
|
||||||
|
/** 当前执行轮次,从1开始 */
|
||||||
|
currentRoundIndex?: number;
|
||||||
|
/** 开始时间 */
|
||||||
|
startTime?: string;
|
||||||
|
/** 结束时间 */
|
||||||
|
endTime?: string;
|
||||||
|
/** 执行状态:0执行中 1执行完成 2执行失败 3已停止 */
|
||||||
|
status?: number;
|
||||||
|
/** 失败原因 */
|
||||||
|
failReason?: string;
|
||||||
|
/** 当前执行节点id */
|
||||||
|
currentNodeId?: string;
|
||||||
|
/** 当前执行的动作id */
|
||||||
|
currentActionId?: string;
|
||||||
|
/** 开始执行时的小车电量 */
|
||||||
|
startBatteryLevel?: number;
|
||||||
|
/** 执行结束后小车电量 */
|
||||||
|
endBatteryLevel?: number;
|
||||||
|
/** 开始执行时的小车位姿x */
|
||||||
|
startPoseX?: number;
|
||||||
|
/** 开始执行时的小车位姿y */
|
||||||
|
startPoseY?: number;
|
||||||
|
/** 执行结束时的小车位姿x */
|
||||||
|
endPoseX?: number;
|
||||||
|
/** 执行结束时的小车位姿y */
|
||||||
|
endPoseY?: number;
|
||||||
|
/** 总动作数 */
|
||||||
|
totalActionCount?: number;
|
||||||
|
/** 当前节点序号 */
|
||||||
|
currentNodeIndex?: number;
|
||||||
|
/** 创建时间 */
|
||||||
|
createTime?: string;
|
||||||
|
/** 更新时间 */
|
||||||
|
updateTime?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 巡检任务节点模板,对应后端 com.haizhiyustc.ros.dto.RosInspectionTaskNodeDTO
|
||||||
|
* type: via_point 途经点 / action_point 任务点
|
||||||
|
*/
|
||||||
|
export interface RosInspectionTaskNodeDTO {
|
||||||
|
/** 节点类型:via_point 途经点 / action_point 任务点 */
|
||||||
|
type?: string;
|
||||||
|
/** 位姿x */
|
||||||
|
poseX?: number;
|
||||||
|
/** 位姿y */
|
||||||
|
poseY?: number;
|
||||||
|
/** 节点顺序 */
|
||||||
|
nodeIndex?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 巡检任务轮次模板,对应后端 com.haizhiyustc.ros.dto.RosInspectionTaskRoundDTO
|
||||||
|
* direction: forward 正走 / backward 倒走
|
||||||
|
*/
|
||||||
|
export interface RosInspectionTaskRoundDTO {
|
||||||
|
/** 轮次,从1开始 */
|
||||||
|
roundIndex?: number;
|
||||||
|
/** 本轮行走方向:forward 正走 / backward 倒走 */
|
||||||
|
direction?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 巡检任务动作模板,对应后端 RosInspectionTaskDetailDTO.RosInspectionActionDTO
|
||||||
|
* command: 0升降柱 1云台旋转 2抓拍识别
|
||||||
|
*/
|
||||||
|
export interface RosInspectionActionDTO {
|
||||||
|
/** 节点顺序 */
|
||||||
|
nodeIndex?: number;
|
||||||
|
/** 轮次 */
|
||||||
|
roundIndex?: number;
|
||||||
|
/** 动作类型:0升降柱 1云台旋转 2抓拍识别 */
|
||||||
|
command?: number;
|
||||||
|
/** 动作参数 */
|
||||||
|
params?: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 巡检任务模板详情,对应后端 com.haizhiyustc.ros.dto.RosInspectionTaskDetailDTO
|
||||||
|
*/
|
||||||
|
export interface RosInspectionTaskDetailDTO {
|
||||||
|
/** 主键id */
|
||||||
|
id?: string;
|
||||||
|
/** 巡检设备id */
|
||||||
|
carId?: string;
|
||||||
|
/** 巡检说明 */
|
||||||
|
remark?: string;
|
||||||
|
/** 巡检状态:0待执行 1执行中 2已停止 */
|
||||||
|
status?: number;
|
||||||
|
/** 全局限速 */
|
||||||
|
maxVelocity?: number;
|
||||||
|
/** ROS2端生成平滑曲线时的插值密度(米/点) */
|
||||||
|
interpolationDensity?: number;
|
||||||
|
/** 巡检地图名 */
|
||||||
|
mapName?: string;
|
||||||
|
/** 巡检轮数 */
|
||||||
|
roundCount?: number;
|
||||||
|
/** 巡检节点模板列表 */
|
||||||
|
nodes?: RosInspectionTaskNodeDTO[];
|
||||||
|
/** 巡检轮次模板列表 */
|
||||||
|
rounds?: RosInspectionTaskRoundDTO[];
|
||||||
|
/** 巡检动作模板列表 */
|
||||||
|
actions?: RosInspectionActionDTO[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 巡检任务下发结果,对应后端 com.haizhiyustc.ros.dto.RosDispatchResultDTO
|
||||||
|
*/
|
||||||
|
export interface RosDispatchResultDTO {
|
||||||
|
/** 巡检记录id */
|
||||||
|
recordId?: string;
|
||||||
|
/** 小车是否接受本次下发 */
|
||||||
|
accepted?: boolean;
|
||||||
|
/** 下发结果说明 */
|
||||||
|
message?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 小车分页查询条件,对应后端 com.haizhiyustc.ros.entity.RosCar 的字段过滤
|
||||||
|
*/
|
||||||
|
export type RosCarQuery = Partial<RosCar> & {
|
||||||
|
/** 关键字 */
|
||||||
|
keyword?: string;
|
||||||
|
};
|
||||||
|
|
||||||
// 小车分页列表
|
// 小车分页列表
|
||||||
export function getRosCarList(data: unknown) {
|
export function getRosCarList(data: PageParam<RosCarQuery>) {
|
||||||
return request({
|
return request<ApiResult<PageResult<RosCar>>>({
|
||||||
url: "/admin/rosCar/list",
|
url: "/admin/rosCar/list",
|
||||||
data,
|
data,
|
||||||
});
|
});
|
||||||
@@ -10,7 +232,7 @@ export function getRosCarList(data: unknown) {
|
|||||||
|
|
||||||
// 保存巡检任务模板
|
// 保存巡检任务模板
|
||||||
export function saveRosInspectionTask(data: unknown) {
|
export function saveRosInspectionTask(data: unknown) {
|
||||||
return request({
|
return request<ApiResult<null>>({
|
||||||
url: "/admin/rosInspectionTask/save",
|
url: "/admin/rosInspectionTask/save",
|
||||||
data,
|
data,
|
||||||
});
|
});
|
||||||
@@ -18,7 +240,7 @@ export function saveRosInspectionTask(data: unknown) {
|
|||||||
|
|
||||||
// 巡检任务模板详情
|
// 巡检任务模板详情
|
||||||
export function getRosInspectionTaskDetail(cardId: string) {
|
export function getRosInspectionTaskDetail(cardId: string) {
|
||||||
return request({
|
return request<ApiResult<RosInspectionTaskDetailDTO>>({
|
||||||
url: "/admin/rosInspectionTask/detail",
|
url: "/admin/rosInspectionTask/detail",
|
||||||
params: { cardId },
|
params: { cardId },
|
||||||
});
|
});
|
||||||
@@ -26,7 +248,7 @@ export function getRosInspectionTaskDetail(cardId: string) {
|
|||||||
|
|
||||||
// 查询小车当前巡检状态
|
// 查询小车当前巡检状态
|
||||||
export function getCurrentRosInspection(carId: string) {
|
export function getCurrentRosInspection(carId: string) {
|
||||||
return request({
|
return request<ApiResult<RosInspectionRecord>>({
|
||||||
url: "/admin/rosCar/currentInspection",
|
url: "/admin/rosCar/currentInspection",
|
||||||
params: { carId },
|
params: { carId },
|
||||||
});
|
});
|
||||||
@@ -34,7 +256,7 @@ export function getCurrentRosInspection(carId: string) {
|
|||||||
|
|
||||||
// 下发巡检任务
|
// 下发巡检任务
|
||||||
export function dispatchRosInspectionTask(taskId: string) {
|
export function dispatchRosInspectionTask(taskId: string) {
|
||||||
return request({
|
return request<ApiResult<RosDispatchResultDTO>>({
|
||||||
url: "/admin/rosInspectionTask/dispatch",
|
url: "/admin/rosInspectionTask/dispatch",
|
||||||
params: { taskId },
|
params: { taskId },
|
||||||
});
|
});
|
||||||
@@ -42,7 +264,7 @@ export function dispatchRosInspectionTask(taskId: string) {
|
|||||||
|
|
||||||
// 停止已下发的巡检任务
|
// 停止已下发的巡检任务
|
||||||
export function stopDispatchRosInspectionTask(taskId: string) {
|
export function stopDispatchRosInspectionTask(taskId: string) {
|
||||||
return request({
|
return request<ApiResult<RosDispatchResultDTO>>({
|
||||||
url: "/admin/rosInspectionTask/stopDispatch",
|
url: "/admin/rosInspectionTask/stopDispatch",
|
||||||
params: { taskId },
|
params: { taskId },
|
||||||
});
|
});
|
||||||
@@ -54,7 +276,7 @@ export function uploadFile(file: File) {
|
|||||||
|
|
||||||
data.append("file", file);
|
data.append("file", file);
|
||||||
|
|
||||||
return request({
|
return request<ApiResult<string>>({
|
||||||
url: "/admin/file/upload",
|
url: "/admin/file/upload",
|
||||||
data,
|
data,
|
||||||
});
|
});
|
||||||
@@ -66,7 +288,7 @@ export function updateRosCarMapUrls(data: {
|
|||||||
mapPgmUrl?: string;
|
mapPgmUrl?: string;
|
||||||
mapYamlUrl?: string;
|
mapYamlUrl?: string;
|
||||||
}) {
|
}) {
|
||||||
return request({
|
return request<ApiResult<null>>({
|
||||||
url: "/admin/rosCar/updateMapUrls",
|
url: "/admin/rosCar/updateMapUrls",
|
||||||
data,
|
data,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,52 +0,0 @@
|
|||||||
import request, { BASE_URL } from '@/utils/request'
|
|
||||||
export function getPublicKey() {
|
|
||||||
return request({
|
|
||||||
url: 'admin/account/publicKey'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
export function login(data) {
|
|
||||||
return request({
|
|
||||||
url: 'admin/account/login',
|
|
||||||
data
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function captcha() {
|
|
||||||
return BASE_URL + '/admin/account/captcha'
|
|
||||||
}
|
|
||||||
export function getInfo() {
|
|
||||||
return request({
|
|
||||||
url: 'admin/account/info',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function updateInfo(data) {
|
|
||||||
return request({
|
|
||||||
url: 'admin/account/update',
|
|
||||||
data
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function updatePwd(data) {
|
|
||||||
return request({
|
|
||||||
url: 'admin/account/updatePwd',
|
|
||||||
data
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 发送重置密码验证码
|
|
||||||
export function getVerifyCode(data) {
|
|
||||||
return request({
|
|
||||||
url: '/admin/sysUser/getVerifyCode',
|
|
||||||
data
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
//忘记密码
|
|
||||||
export function resetUserPassword(data) {
|
|
||||||
return request({
|
|
||||||
url: '/admin/sysUser/resetUserPassword',
|
|
||||||
data
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
105
src/api/user.ts
Normal file
105
src/api/user.ts
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
import request, { BASE_URL } from '@/utils/request'
|
||||||
|
import type { ApiResult } from '@/types/api'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 登录返回,对应后端 com.haizhiyustc.system.vo.SysUserLoginVO
|
||||||
|
*/
|
||||||
|
export interface LoginResult {
|
||||||
|
/** 用户id */
|
||||||
|
userId: string;
|
||||||
|
/** 登录令牌 */
|
||||||
|
token: string;
|
||||||
|
/** 令牌请求头名称 */
|
||||||
|
tokenName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户个人资料,对应后端 UserInfoVO.UserInfoProfile
|
||||||
|
*/
|
||||||
|
export interface UserProfile {
|
||||||
|
/** 账号id */
|
||||||
|
id?: string;
|
||||||
|
/** 头像 */
|
||||||
|
avatar?: string;
|
||||||
|
/** 登录账号 */
|
||||||
|
account?: string;
|
||||||
|
/** 账户名 */
|
||||||
|
name?: string;
|
||||||
|
/** 性别 */
|
||||||
|
gender?: string;
|
||||||
|
/** 邮箱 */
|
||||||
|
email?: string;
|
||||||
|
/** 手机号 */
|
||||||
|
phone?: string;
|
||||||
|
/** 状态 */
|
||||||
|
status?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当前登录用户信息,对应后端 com.haizhiyustc.system.vo.UserInfoVO
|
||||||
|
*/
|
||||||
|
export interface UserInfoVO {
|
||||||
|
/** 账号名 */
|
||||||
|
name?: string;
|
||||||
|
/** 角色 */
|
||||||
|
role?: string;
|
||||||
|
/** 角色列表 */
|
||||||
|
roles?: string[];
|
||||||
|
/** 权限列表 */
|
||||||
|
permissions?: string[];
|
||||||
|
/** 用户个人资料 */
|
||||||
|
profile?: UserProfile;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPublicKey() {
|
||||||
|
return request<ApiResult<string>>({
|
||||||
|
url: 'admin/account/publicKey'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function login(data: Record<string, unknown>) {
|
||||||
|
return request<ApiResult<LoginResult>>({
|
||||||
|
url: 'admin/account/login',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function captcha(): string {
|
||||||
|
return BASE_URL + '/admin/account/captcha'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getInfo() {
|
||||||
|
return request<ApiResult<UserInfoVO>>({
|
||||||
|
url: 'admin/account/info',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateInfo(data: Record<string, unknown>) {
|
||||||
|
return request<ApiResult<unknown>>({
|
||||||
|
url: 'admin/account/update',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updatePwd(data: Record<string, unknown>) {
|
||||||
|
return request<ApiResult<unknown>>({
|
||||||
|
url: 'admin/account/updatePwd',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送重置密码验证码
|
||||||
|
export function getVerifyCode(data: Record<string, unknown>) {
|
||||||
|
return request<ApiResult<unknown>>({
|
||||||
|
url: '/admin/sysUser/getVerifyCode',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
//忘记密码
|
||||||
|
export function resetUserPassword(data: Record<string, unknown>) {
|
||||||
|
return request<ApiResult<unknown>>({
|
||||||
|
url: '/admin/sysUser/resetUserPassword',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
21
src/components/ProviderHelper.vue
Normal file
21
src/components/ProviderHelper.vue
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<template>
|
||||||
|
<div></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { useDialog, useLoadingBar, useMessage } from 'naive-ui'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* naive-ui的弹窗类的都没有提供全局方法, 这里挂载到window对象上
|
||||||
|
*/
|
||||||
|
export default {
|
||||||
|
name: 'ProviderHelper',
|
||||||
|
setup() {
|
||||||
|
window.$message = useMessage()
|
||||||
|
window.$loadingBar = useLoadingBar()
|
||||||
|
window.$modal = useDialog()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { onBeforeUnmount, onMounted, ref, watch } from "vue";
|
import { onBeforeUnmount, onMounted, ref, watch } from "vue";
|
||||||
import * as echarts from "echarts";
|
import { init } from "echarts/core";
|
||||||
import { EChartsOption } from "echarts";
|
import type { EChartsOption } from "echarts";
|
||||||
|
|
||||||
let chart: echarts.ECharts | null = null;
|
let chart: ReturnType<typeof init> | null = null;
|
||||||
|
|
||||||
const chartRef = ref<HTMLElement | undefined>();
|
const chartRef = ref<HTMLElement | undefined>();
|
||||||
|
|
||||||
@@ -24,7 +24,7 @@ function applyOption(option?: EChartsOption | Record<string, never>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
chart = echarts.init(chartRef.value, "t-theme");
|
chart = init(chartRef.value, "t-theme");
|
||||||
applyOption(props.option);
|
applyOption(props.option);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
13
src/main.ts
13
src/main.ts
@@ -2,17 +2,18 @@ import { createApp } from "vue";
|
|||||||
import "./style.less";
|
import "./style.less";
|
||||||
import App from "./App.vue";
|
import App from "./App.vue";
|
||||||
import Chart from "./components/chart/index.vue";
|
import Chart from "./components/chart/index.vue";
|
||||||
import * as echarts from "echarts";
|
import { registerTheme } from "echarts/core";
|
||||||
|
import { CanvasRenderer } from "echarts/renderers";
|
||||||
import echartsConfig from "./config/echarts.config";
|
import echartsConfig from "./config/echarts.config";
|
||||||
import { createDiscreteApi } from "naive-ui";
|
|
||||||
import { createPinia } from "pinia";
|
import { createPinia } from "pinia";
|
||||||
import router from "./router";
|
import router from "./router";
|
||||||
|
import "./router/routerGuard";
|
||||||
|
|
||||||
echarts.registerTheme("t-theme", echartsConfig);
|
// CanvasRenderer 注册到全局,所有图表都需要
|
||||||
|
import { use } from "echarts/core";
|
||||||
|
use([CanvasRenderer]);
|
||||||
|
|
||||||
const { message, dialog } = createDiscreteApi(["message", "dialog"]);
|
registerTheme("t-theme", echartsConfig);
|
||||||
window.$message = message;
|
|
||||||
window.$modal = dialog;
|
|
||||||
|
|
||||||
const app = createApp(App);
|
const app = createApp(App);
|
||||||
|
|
||||||
|
|||||||
@@ -75,21 +75,6 @@ const router = createRouter({
|
|||||||
routes,
|
routes,
|
||||||
});
|
});
|
||||||
|
|
||||||
const whiteList = ["/login"];
|
|
||||||
|
|
||||||
router.beforeEach((to) => {
|
|
||||||
const hasToken = Boolean(getToken());
|
|
||||||
|
|
||||||
if (!hasToken && !whiteList.includes(to.path)) {
|
|
||||||
return {
|
|
||||||
path: "/login",
|
|
||||||
query: { redirect: to.fullPath },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
export function resetRouter() {
|
export function resetRouter() {
|
||||||
// 当前项目暂无动态 addRoute 路由,保留该方法兼容登出流程。
|
// 当前项目暂无动态 addRoute 路由,保留该方法兼容登出流程。
|
||||||
}
|
}
|
||||||
|
|||||||
60
src/router/routerGuard.ts
Normal file
60
src/router/routerGuard.ts
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import router from './index'
|
||||||
|
import { getToken } from '@/utils/auth'
|
||||||
|
import { useUserStore } from '@/store/modules/user'
|
||||||
|
|
||||||
|
const whiteList = ['/login'] // no redirect whitelist
|
||||||
|
|
||||||
|
// const setPageTitle = function (meta) {
|
||||||
|
// let title = '海灵标'
|
||||||
|
// if (meta && meta.title) {
|
||||||
|
// title = `${meta.title}`
|
||||||
|
// }
|
||||||
|
// document.title = title
|
||||||
|
// }
|
||||||
|
// 路由守卫
|
||||||
|
router.beforeEach(async (to, from) => {
|
||||||
|
$loadingBar.start()
|
||||||
|
// setPageTitle(to.meta)
|
||||||
|
const hasToken = getToken()
|
||||||
|
const userStore = useUserStore()
|
||||||
|
if (hasToken) {
|
||||||
|
if (to.path === '/login') {
|
||||||
|
$loadingBar.finish()
|
||||||
|
return { path: '/' }
|
||||||
|
} else {
|
||||||
|
const hasGetUserInfo = userStore.name
|
||||||
|
if (hasGetUserInfo !== '') {
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
// get user info
|
||||||
|
const infoVO = await userStore.getInfo()
|
||||||
|
return { ...to, replace: true }
|
||||||
|
} catch (error) {
|
||||||
|
await userStore.resetToken()
|
||||||
|
// remove token and go to login page to re-login
|
||||||
|
console.error(error)
|
||||||
|
$loadingBar.error()
|
||||||
|
return `/login?redirect=${to.path}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
/* has no token */
|
||||||
|
if (whiteList.indexOf(to.path) !== -1) {
|
||||||
|
// in the free login whitelist, go directly
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
$loadingBar.finish()
|
||||||
|
return `/login?redirect=${to.path}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
router.afterEach((to) => {
|
||||||
|
if (to.matched.length === 0) {
|
||||||
|
router.push('/404')
|
||||||
|
// fixme 不能自动跳转404, 需要引导
|
||||||
|
}
|
||||||
|
$loadingBar.finish()
|
||||||
|
})
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import { createPinia } from 'pinia'
|
|
||||||
|
|
||||||
export function setupStore(app) {
|
|
||||||
app.use(createPinia())
|
|
||||||
}
|
|
||||||
6
src/store/index.ts
Normal file
6
src/store/index.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { createPinia } from 'pinia'
|
||||||
|
import type { App } from 'vue'
|
||||||
|
|
||||||
|
export function setupStore(app: App) {
|
||||||
|
app.use(createPinia())
|
||||||
|
}
|
||||||
@@ -1,21 +1,31 @@
|
|||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { nextTick } from 'vue'
|
import { nextTick } from 'vue'
|
||||||
import { useDark } from '@vueuse/core'
|
|
||||||
|
|
||||||
const sidebarStatusKey = 'sidebar_status'
|
const sidebarStatusKey = 'sidebar_status'
|
||||||
|
|
||||||
|
interface AppState {
|
||||||
|
/** 侧边栏是否展开,持久化在 localStorage */
|
||||||
|
sidebarStatus: boolean
|
||||||
|
/** 当前设备类型,desktop/mobile */
|
||||||
|
device: string
|
||||||
|
/** 应用配置 */
|
||||||
|
config: Record<string, unknown>
|
||||||
|
/** 页面刷新标记,reloadPage 控制 true/false 触发重新渲染 */
|
||||||
|
reloadFlag: boolean
|
||||||
|
/** 是否暗色模式 */
|
||||||
|
isDark: boolean
|
||||||
|
}
|
||||||
|
|
||||||
export const useAppStore = defineStore('app', {
|
export const useAppStore = defineStore('app', {
|
||||||
state() {
|
state: (): AppState => ({
|
||||||
return {
|
sidebarStatus: localStorage.getItem(sidebarStatusKey) ? localStorage.getItem(sidebarStatusKey) === '1' : true,
|
||||||
sidebarStatus: localStorage.getItem(sidebarStatusKey) ? localStorage.getItem(sidebarStatusKey) === '1' : true,
|
device: 'desktop',
|
||||||
device: 'desktop',
|
config: {},
|
||||||
config: {},
|
reloadFlag: true,
|
||||||
reloadFlag: true,
|
isDark: !window.matchMedia('(prefers-color-scheme: dark)').matches,
|
||||||
isDark: !useDark(),
|
}),
|
||||||
}
|
|
||||||
},
|
|
||||||
getters: {
|
getters: {
|
||||||
sidebar() {
|
sidebar(): boolean {
|
||||||
return this.sidebarStatus
|
return this.sidebarStatus
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -28,11 +38,11 @@ export const useAppStore = defineStore('app', {
|
|||||||
localStorage.setItem(sidebarStatusKey, '0')
|
localStorage.setItem(sidebarStatusKey, '0')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
closeSideBar(withoutAnimation) {
|
closeSideBar(withoutAnimation: boolean) {
|
||||||
localStorage.setItem(sidebarStatusKey, '0')
|
localStorage.setItem(sidebarStatusKey, '0')
|
||||||
this.sidebarStatus = false
|
this.sidebarStatus = false
|
||||||
},
|
},
|
||||||
toggleDevice(device) {
|
toggleDevice(device: string) {
|
||||||
this.device = device
|
this.device = device
|
||||||
},
|
},
|
||||||
async reloadPage() {
|
async reloadPage() {
|
||||||
@@ -1,112 +0,0 @@
|
|||||||
import { getToken, removeToken, setToken } from "@/utils/auth.js";
|
|
||||||
import { getInfo, login as doLogin } from "@/api/user.js";
|
|
||||||
import { defineStore } from "pinia";
|
|
||||||
import { resetRouter } from "@/router";
|
|
||||||
|
|
||||||
export const useUserStore = defineStore("user", {
|
|
||||||
state() {
|
|
||||||
return {
|
|
||||||
userInfo: {
|
|
||||||
token: getToken() || "",
|
|
||||||
id: "",
|
|
||||||
avatar: "",
|
|
||||||
name: "",
|
|
||||||
profile: {},
|
|
||||||
permissions: [],
|
|
||||||
dictMap: {},
|
|
||||||
dictTreeMap: {},
|
|
||||||
dictIdMap: {},
|
|
||||||
dynamicAttrs: [],
|
|
||||||
companyLogStatus: null,
|
|
||||||
attrIds: [],
|
|
||||||
regionIds: [],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
getters: {
|
|
||||||
userId() {
|
|
||||||
return this.userInfo?.id || "";
|
|
||||||
},
|
|
||||||
name() {
|
|
||||||
return this.userInfo?.name || "";
|
|
||||||
},
|
|
||||||
avatar() {
|
|
||||||
return this.userInfo?.profile?.avatar || "";
|
|
||||||
},
|
|
||||||
permissions() {
|
|
||||||
return this.userInfo?.permissions || [];
|
|
||||||
},
|
|
||||||
dictMap() {
|
|
||||||
return this.userInfo?.dictMap || {};
|
|
||||||
},
|
|
||||||
dictTreeMap() {
|
|
||||||
return this.userInfo?.dictTreeMap || {};
|
|
||||||
},
|
|
||||||
dictIdMap() {
|
|
||||||
return this.userInfo?.dictIdMap || {};
|
|
||||||
},
|
|
||||||
dynamicAttrs() {
|
|
||||||
return this.userInfo?.dynamicAttrs || [];
|
|
||||||
},
|
|
||||||
companyLogStatus() {
|
|
||||||
return this.userInfo?.companyLogStatus || 0;
|
|
||||||
},
|
|
||||||
attrIds() {
|
|
||||||
return this.userInfo?.attrIds || [];
|
|
||||||
},
|
|
||||||
regionIds() {
|
|
||||||
return this.userInfo?.regionIds || [];
|
|
||||||
},
|
|
||||||
},
|
|
||||||
actions: {
|
|
||||||
async login(userInfo) {
|
|
||||||
const res = await doLogin(userInfo);
|
|
||||||
const { token, tokenName } = res.data;
|
|
||||||
this.userInfo.token = token;
|
|
||||||
setToken(token, tokenName);
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
async getInfo() {
|
|
||||||
const res = await getInfo();
|
|
||||||
const data = res.data;
|
|
||||||
const {
|
|
||||||
name,
|
|
||||||
profile,
|
|
||||||
permissions,
|
|
||||||
dynamicAttrs,
|
|
||||||
attrIds,
|
|
||||||
companyLogStatus,
|
|
||||||
regionIds,
|
|
||||||
} = data;
|
|
||||||
|
|
||||||
// 保留当前的 token,避免被覆盖
|
|
||||||
const currentToken = this.userInfo.token || getToken();
|
|
||||||
|
|
||||||
this.userInfo = {
|
|
||||||
token: currentToken,
|
|
||||||
id: profile.id,
|
|
||||||
name,
|
|
||||||
profile,
|
|
||||||
permissions,
|
|
||||||
dynamicAttrs,
|
|
||||||
attrIds,
|
|
||||||
companyLogStatus,
|
|
||||||
regionIds,
|
|
||||||
};
|
|
||||||
return data;
|
|
||||||
},
|
|
||||||
// user logout
|
|
||||||
async logout() {
|
|
||||||
await this.resetToken();
|
|
||||||
await resetRouter();
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
async resetToken() {
|
|
||||||
this.userInfo = {
|
|
||||||
profile: {},
|
|
||||||
permissions: [],
|
|
||||||
};
|
|
||||||
removeToken();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
73
src/store/modules/user.ts
Normal file
73
src/store/modules/user.ts
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import { getToken, removeToken, setToken } from "@/utils/auth";
|
||||||
|
import { getInfo, login as doLogin } from "@/api/user";
|
||||||
|
import type { UserInfoVO } from "@/api/user";
|
||||||
|
import { defineStore } from "pinia";
|
||||||
|
import { resetRouter } from "@/router";
|
||||||
|
|
||||||
|
// store 侧用户信息:后端返回结构 + 登录令牌,token 独立维护
|
||||||
|
type UserInfo = Partial<UserInfoVO> & {
|
||||||
|
token?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useUserStore = defineStore("user", {
|
||||||
|
state: (): { userInfo: UserInfo } => ({
|
||||||
|
userInfo: {
|
||||||
|
token: getToken() || "",
|
||||||
|
name: "",
|
||||||
|
profile: {},
|
||||||
|
permissions: [],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
getters: {
|
||||||
|
userId(): string {
|
||||||
|
return this.userInfo?.profile?.id || "";
|
||||||
|
},
|
||||||
|
name(): string {
|
||||||
|
return this.userInfo?.name || "";
|
||||||
|
},
|
||||||
|
avatar(): string {
|
||||||
|
return this.userInfo?.profile?.avatar || "";
|
||||||
|
},
|
||||||
|
permissions(): string[] {
|
||||||
|
return this.userInfo?.permissions || [];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
async login(userInfo: Record<string, unknown>) {
|
||||||
|
const res = await doLogin(userInfo);
|
||||||
|
const { token, tokenName } = res.data;
|
||||||
|
this.userInfo.token = token;
|
||||||
|
setToken(token, tokenName);
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
async getInfo() {
|
||||||
|
const res = await getInfo();
|
||||||
|
const data = res.data;
|
||||||
|
const { name, profile, permissions } = data;
|
||||||
|
|
||||||
|
// 保留当前的 token,避免被覆盖
|
||||||
|
const currentToken = this.userInfo.token || getToken();
|
||||||
|
|
||||||
|
this.userInfo = {
|
||||||
|
token: currentToken,
|
||||||
|
name,
|
||||||
|
profile,
|
||||||
|
permissions,
|
||||||
|
};
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
// user logout
|
||||||
|
async logout() {
|
||||||
|
await this.resetToken();
|
||||||
|
await resetRouter();
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
async resetToken() {
|
||||||
|
this.userInfo = {
|
||||||
|
profile: {},
|
||||||
|
permissions: [],
|
||||||
|
};
|
||||||
|
removeToken();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
48
src/types/api.ts
Normal file
48
src/types/api.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
/**
|
||||||
|
* 后端统一返回结构,对应后端 com.haizhiyustc.common.web.api.ApiResult
|
||||||
|
* code: 200 成功 / 401 未登录或登录过期 / 500 失败
|
||||||
|
*/
|
||||||
|
export interface ApiResult<T = unknown> {
|
||||||
|
/** 状态码 */
|
||||||
|
code: number;
|
||||||
|
/** 返回的数据 */
|
||||||
|
data: T;
|
||||||
|
/** 说明信息 */
|
||||||
|
msg: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页结果,对应后端 easy-query 的 EasyPageResult(仅返回 data 与 total)
|
||||||
|
*/
|
||||||
|
export interface PageResult<T> {
|
||||||
|
/** 符合条件的总记录数 */
|
||||||
|
total?: number;
|
||||||
|
/** 当前页数据列表 */
|
||||||
|
data?: T[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 排序项,对应后端 com.haizhiyustc.common.crud.bean.OrderItem
|
||||||
|
*/
|
||||||
|
export interface OrderItem {
|
||||||
|
/** 排序属性名 */
|
||||||
|
property: string;
|
||||||
|
/** 关联表序号 */
|
||||||
|
tableIndex?: number;
|
||||||
|
/** 是否升序,默认 false 降序 */
|
||||||
|
asc?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页请求参数,对应后端 com.haizhiyustc.common.crud.bean.PageParam
|
||||||
|
*/
|
||||||
|
export interface PageParam<T> {
|
||||||
|
/** 页码,从1开始 */
|
||||||
|
page?: number;
|
||||||
|
/** 每页条数 */
|
||||||
|
limit?: number;
|
||||||
|
/** 排序列表 */
|
||||||
|
orderList?: OrderItem[];
|
||||||
|
/** 查询条件 */
|
||||||
|
query?: T;
|
||||||
|
}
|
||||||
55
src/types/js-modules.d.ts
vendored
55
src/types/js-modules.d.ts
vendored
@@ -9,55 +9,12 @@ interface Window {
|
|||||||
create(options: Record<string, unknown>): unknown;
|
create(options: Record<string, unknown>): unknown;
|
||||||
warning(options: Record<string, unknown>): unknown;
|
warning(options: Record<string, unknown>): unknown;
|
||||||
};
|
};
|
||||||
}
|
$loadingBar: {
|
||||||
|
start(): void;
|
||||||
declare module "@/utils/auth" {
|
finish(): void;
|
||||||
export function getToken(): string | null;
|
error(): void;
|
||||||
export function getTokenName(): string | null;
|
[key: string]: unknown;
|
||||||
export function setToken(tokenValue: string, tokenNameValue?: string): void;
|
|
||||||
export function removeToken(): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
declare module "@/utils/auth.js" {
|
|
||||||
export function getToken(): string | null;
|
|
||||||
export function getTokenName(): string | null;
|
|
||||||
export function setToken(tokenValue: string, tokenNameValue?: string): void;
|
|
||||||
export function removeToken(): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
declare module "@/store/modules/user" {
|
|
||||||
export function useUserStore(): {
|
|
||||||
login(userInfo: Record<string, unknown>): Promise<boolean>;
|
|
||||||
logout(): Promise<boolean>;
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module "@/store/modules/user.js" {
|
declare var $loadingBar: Window["$loadingBar"];
|
||||||
export function useUserStore(): {
|
|
||||||
login(userInfo: Record<string, unknown>): Promise<boolean>;
|
|
||||||
logout(): Promise<boolean>;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
declare module "@/utils/passwordEncrypt.js" {
|
|
||||||
export function encryptPasswordPayload<T extends Record<string, unknown>>(
|
|
||||||
payload: T,
|
|
||||||
fields: Array<keyof T>,
|
|
||||||
): Promise<T>;
|
|
||||||
}
|
|
||||||
|
|
||||||
declare module "@/utils/request" {
|
|
||||||
import type { AxiosRequestConfig } from "axios";
|
|
||||||
|
|
||||||
export const BASE_URL: string;
|
|
||||||
const request: <T = unknown>(config: AxiosRequestConfig) => Promise<T>;
|
|
||||||
export default request;
|
|
||||||
}
|
|
||||||
|
|
||||||
declare module "@/utils/request.js" {
|
|
||||||
import type { AxiosRequestConfig } from "axios";
|
|
||||||
|
|
||||||
export const BASE_URL: string;
|
|
||||||
const request: <T = unknown>(config: AxiosRequestConfig) => Promise<T>;
|
|
||||||
export default request;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
const TokenKey = "token";
|
const TokenKey = "token";
|
||||||
const TOKEN_NAME_KEY = "tokenName";
|
const TOKEN_NAME_KEY = "tokenName";
|
||||||
|
|
||||||
let token = null;
|
let token: string | null = null;
|
||||||
let tokenName = null;
|
let tokenName: string | null = null;
|
||||||
|
|
||||||
export function getToken() {
|
export function getToken(): string | null {
|
||||||
const storedToken = token || window.localStorage.getItem(TokenKey);
|
const storedToken = token || window.localStorage.getItem(TokenKey);
|
||||||
if (!storedToken || storedToken === "null" || storedToken === "undefined") {
|
if (!storedToken || storedToken === "null" || storedToken === "undefined") {
|
||||||
token = null;
|
token = null;
|
||||||
@@ -15,21 +15,21 @@ export function getToken() {
|
|||||||
return token;
|
return token;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setToken(tokenValue, tokenNameValue) {
|
export function setToken(tokenValue: string, tokenNameValue?: string): void {
|
||||||
token = tokenValue;
|
token = tokenValue;
|
||||||
tokenName = tokenNameValue || "Authorization";
|
tokenName = tokenNameValue || "Authorization";
|
||||||
window.localStorage.setItem(TokenKey, token);
|
window.localStorage.setItem(TokenKey, token);
|
||||||
window.localStorage.setItem(TOKEN_NAME_KEY, tokenName);
|
window.localStorage.setItem(TOKEN_NAME_KEY, tokenName);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function removeToken() {
|
export function removeToken(): void {
|
||||||
token = null;
|
token = null;
|
||||||
tokenName = null;
|
tokenName = null;
|
||||||
window.localStorage.removeItem(TokenKey);
|
window.localStorage.removeItem(TokenKey);
|
||||||
window.localStorage.removeItem(TOKEN_NAME_KEY);
|
window.localStorage.removeItem(TOKEN_NAME_KEY);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getTokenName() {
|
export function getTokenName(): string | null {
|
||||||
tokenName = tokenName || window.localStorage.getItem(TOKEN_NAME_KEY);
|
tokenName = tokenName || window.localStorage.getItem(TOKEN_NAME_KEY);
|
||||||
return tokenName;
|
return tokenName;
|
||||||
}
|
}
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
import { JSEncrypt } from 'jsencrypt'
|
import { JSEncrypt } from 'jsencrypt'
|
||||||
import { getPublicKey } from '@/api/user'
|
import { getPublicKey } from '@/api/user'
|
||||||
|
|
||||||
export async function getPasswordPublicKey() {
|
export async function getPasswordPublicKey(): Promise<string> {
|
||||||
const res = await getPublicKey()
|
const res = await getPublicKey()
|
||||||
return res.data
|
return res.data
|
||||||
}
|
}
|
||||||
|
|
||||||
export function encryptPassword(plainText, publicKey) {
|
export function encryptPassword(plainText: string, publicKey: string): string {
|
||||||
const encryptor = new JSEncrypt()
|
const encryptor = new JSEncrypt()
|
||||||
encryptor.setPublicKey(formatPublicKey(publicKey))
|
encryptor.setPublicKey(formatPublicKey(publicKey))
|
||||||
const encrypted = encryptor.encrypt(plainText)
|
const encrypted = encryptor.encrypt(plainText)
|
||||||
@@ -16,16 +16,19 @@ export function encryptPassword(plainText, publicKey) {
|
|||||||
return encrypted
|
return encrypted
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function encryptPasswordPayload(payload, fields) {
|
export async function encryptPasswordPayload<T extends Record<string, unknown>>(
|
||||||
|
payload: T,
|
||||||
|
fields: Array<keyof T>,
|
||||||
|
): Promise<T> {
|
||||||
const publicKey = await getPasswordPublicKey()
|
const publicKey = await getPasswordPublicKey()
|
||||||
const nextPayload = { ...payload }
|
const nextPayload = { ...payload }
|
||||||
for (const field of fields) {
|
for (const field of fields) {
|
||||||
nextPayload[field] = encryptPassword(payload[field] || '', publicKey)
|
nextPayload[field] = encryptPassword(String(payload[field] || ''), publicKey) as T[keyof T]
|
||||||
}
|
}
|
||||||
return nextPayload
|
return nextPayload
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatPublicKey(publicKey) {
|
function formatPublicKey(publicKey: string): string {
|
||||||
if (!publicKey) {
|
if (!publicKey) {
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import axios from "axios";
|
import axios, { type AxiosRequestConfig } from "axios";
|
||||||
import { useUserStore } from "@/store/modules/user";
|
import { useUserStore } from "@/store/modules/user";
|
||||||
import { getToken, getTokenName } from "@/utils/auth";
|
import { getToken, getTokenName } from "@/utils/auth";
|
||||||
import router from "@/router";
|
import router from "@/router";
|
||||||
@@ -58,9 +58,8 @@ service.interceptors.response.use(
|
|||||||
window.$message.error(error.response.data.msg, {
|
window.$message.error(error.response.data.msg, {
|
||||||
duration: 5 * 1000,
|
duration: 5 * 1000,
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
handleNoLogin();
|
|
||||||
}
|
}
|
||||||
|
handleNoLogin();
|
||||||
} else {
|
} else {
|
||||||
handleNoLogin();
|
handleNoLogin();
|
||||||
}
|
}
|
||||||
@@ -71,16 +70,21 @@ service.interceptors.response.use(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
export default service;
|
// 响应拦截器已解包 response.data,对外暴露的请求方法保持 Promise<T> 语义
|
||||||
|
const request = <T = unknown>(config: AxiosRequestConfig): Promise<T> => {
|
||||||
|
return service.request<any, T>(config);
|
||||||
|
};
|
||||||
|
|
||||||
function showApiMessage(res) {
|
export default request;
|
||||||
|
|
||||||
|
function showApiMessage(res: { msg?: string }) {
|
||||||
window.$message.error(res.msg || "error", {
|
window.$message.error(res.msg || "error", {
|
||||||
duration: 5 * 1000,
|
duration: 5 * 1000,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 弹窗确认防抖
|
// 弹窗确认防抖
|
||||||
let noLoginAlert = null;
|
let noLoginAlert: unknown = null;
|
||||||
|
|
||||||
function handleNoLogin() {
|
function handleNoLogin() {
|
||||||
if (noLoginAlert == null) {
|
if (noLoginAlert == null) {
|
||||||
114
src/utils/rosMapYaml.ts
Normal file
114
src/utils/rosMapYaml.ts
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
export interface ParsedRosMapYaml {
|
||||||
|
image?: string;
|
||||||
|
resolution: number | null;
|
||||||
|
origin: [number, number, number] | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function stripInlineComment(value: string) {
|
||||||
|
let quote: string | null = null;
|
||||||
|
|
||||||
|
for (let index = 0; index < value.length; index += 1) {
|
||||||
|
const char = value[index];
|
||||||
|
const previousChar = value[index - 1];
|
||||||
|
|
||||||
|
if ((char === "'" || char === "\"") && previousChar !== "\\") {
|
||||||
|
quote = quote === char ? null : quote || char;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (char === "#" && !quote) {
|
||||||
|
return value.slice(0, index).trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return value.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function unquoteYamlString(value: string) {
|
||||||
|
const trimmedValue = stripInlineComment(value);
|
||||||
|
|
||||||
|
return trimmedValue.replace(/^['"]|['"]$/g, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
function readScalarValue(content: string, key: string) {
|
||||||
|
const match = content.match(new RegExp(`^\\s*${key}\\s*:\\s*(.*?)\\s*$`, "m"));
|
||||||
|
|
||||||
|
return match ? stripInlineComment(match[1]) : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseFiniteNumber(value: string) {
|
||||||
|
const parsedValue = Number(stripInlineComment(value));
|
||||||
|
|
||||||
|
return Number.isFinite(parsedValue) ? parsedValue : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseInlineOrigin(content: string) {
|
||||||
|
const originValue = content.match(/^\s*origin\s*:\s*\[([^\]]+)\]\s*(?:#.*)?$/m)?.[1];
|
||||||
|
|
||||||
|
if (!originValue) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const values = originValue
|
||||||
|
.split(",")
|
||||||
|
.map((value) => parseFiniteNumber(value.trim()))
|
||||||
|
.slice(0, 3);
|
||||||
|
|
||||||
|
return values.length === 3 && values.every((value) => value !== null)
|
||||||
|
? values as [number, number, number]
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseBlockOrigin(content: string) {
|
||||||
|
const lines = content.split(/\r?\n/);
|
||||||
|
const values: number[] = [];
|
||||||
|
|
||||||
|
for (let index = 0; index < lines.length; index += 1) {
|
||||||
|
const originLine = lines[index].match(/^(\s*)origin\s*:\s*(?:#.*)?$/);
|
||||||
|
|
||||||
|
if (!originLine) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const originIndent = originLine[1].length;
|
||||||
|
|
||||||
|
for (let nextIndex = index + 1; nextIndex < lines.length; nextIndex += 1) {
|
||||||
|
const line = lines[nextIndex];
|
||||||
|
|
||||||
|
if (!line.trim() || /^\s*#/.test(line)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lineIndent = line.match(/^\s*/)?.[0].length ?? 0;
|
||||||
|
if (lineIndent <= originIndent) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const itemValue = line.match(/^\s*-\s*(.*?)\s*$/)?.[1];
|
||||||
|
const parsedValue = itemValue ? parseFiniteNumber(itemValue) : null;
|
||||||
|
|
||||||
|
if (parsedValue === null) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
values.push(parsedValue);
|
||||||
|
|
||||||
|
if (values.length === 3) {
|
||||||
|
return values as [number, number, number];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseRosMapYaml(content: string): ParsedRosMapYaml {
|
||||||
|
const imageValue = readScalarValue(content, "image");
|
||||||
|
const resolution = parseFiniteNumber(readScalarValue(content, "resolution"));
|
||||||
|
const origin = parseInlineOrigin(content) || parseBlockOrigin(content);
|
||||||
|
|
||||||
|
return {
|
||||||
|
image: imageValue ? unquoteYamlString(imageValue) : undefined,
|
||||||
|
resolution: resolution && resolution > 0 ? resolution : null,
|
||||||
|
origin,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -37,7 +37,7 @@ const recognizeTypeMap: Record<number, string> = {
|
|||||||
1: "电缆表面破损",
|
1: "电缆表面破损",
|
||||||
2: "桥架断裂",
|
2: "桥架断裂",
|
||||||
3: "隧道积水",
|
3: "隧道积水",
|
||||||
4: "墙体裂缝",
|
4: "墙体渗漏",
|
||||||
};
|
};
|
||||||
|
|
||||||
const recognitionStatusMap: Record<number, RecognitionStatusText> = {
|
const recognitionStatusMap: Record<number, RecognitionStatusText> = {
|
||||||
@@ -117,6 +117,7 @@ async function fetchAlarmList() {
|
|||||||
const response = (await getAlarmPage({
|
const response = (await getAlarmPage({
|
||||||
page: currentPage.value,
|
page: currentPage.value,
|
||||||
limit: pageSize,
|
limit: pageSize,
|
||||||
|
orderList: [{ property: 'createTime', asc: false }],
|
||||||
query: {
|
query: {
|
||||||
recognizeType: normalizeRecognizeType(recognizeType.value),
|
recognizeType: normalizeRecognizeType(recognizeType.value),
|
||||||
warning: normalizeWarningFilter(warningFilter.value),
|
warning: normalizeWarningFilter(warningFilter.value),
|
||||||
@@ -228,7 +229,7 @@ onMounted(() => {
|
|||||||
<option value="1">电缆表面破损</option>
|
<option value="1">电缆表面破损</option>
|
||||||
<option value="2">桥架断裂</option>
|
<option value="2">桥架断裂</option>
|
||||||
<option value="3">隧道积水</option>
|
<option value="3">隧道积水</option>
|
||||||
<option value="4">墙体裂缝</option>
|
<option value="4">墙体渗漏</option>
|
||||||
</select>
|
</select>
|
||||||
<select v-model="warningFilter" class="toolbar-select">
|
<select v-model="warningFilter" class="toolbar-select">
|
||||||
<option value="">全部预警状态</option>
|
<option value="">全部预警状态</option>
|
||||||
|
|||||||
@@ -3,6 +3,17 @@ import { computed, ref } from "vue";
|
|||||||
import type { EChartsOption } from "echarts";
|
import type { EChartsOption } from "echarts";
|
||||||
import Chart from "@/components/chart/index.vue";
|
import Chart from "@/components/chart/index.vue";
|
||||||
|
|
||||||
|
// 按需注册 DataReportPage 使用的图表类型和组件
|
||||||
|
import { use } from "echarts/core";
|
||||||
|
import { LineChart, PieChart, RadarChart } from "echarts/charts";
|
||||||
|
import {
|
||||||
|
TooltipComponent,
|
||||||
|
GridComponent,
|
||||||
|
LegendComponent,
|
||||||
|
} from "echarts/components";
|
||||||
|
use([LineChart, PieChart, RadarChart]);
|
||||||
|
use([TooltipComponent, GridComponent, LegendComponent]);
|
||||||
|
|
||||||
type AlarmStatus = "待研判" | "待处理" | "处置完成" | "误判";
|
type AlarmStatus = "待研判" | "待处理" | "处置完成" | "误判";
|
||||||
type ProjectName = "西江隧道" | "青云隧道" | "南山隧道";
|
type ProjectName = "西江隧道" | "青云隧道" | "南山隧道";
|
||||||
type DistributionRange = "近一周" | "近一个月" | "近三个月";
|
type DistributionRange = "近一周" | "近一个月" | "近三个月";
|
||||||
@@ -28,22 +39,22 @@ const distributionRange = ref<DistributionRange>("近一周");
|
|||||||
const reportData: AlarmReportRecord[] = [
|
const reportData: AlarmReportRecord[] = [
|
||||||
{ id: "AL-001", type: "线缆破损", robot: "巡检机器人 R1", project: "西江隧道", status: "待研判", time: "2026-06-23 10:24:18" },
|
{ id: "AL-001", type: "线缆破损", robot: "巡检机器人 R1", project: "西江隧道", status: "待研判", time: "2026-06-23 10:24:18" },
|
||||||
{ id: "AL-002", type: "局部高温", robot: "巡检机器人 Q1", project: "青云隧道", status: "待处理", time: "2026-06-23 10:18:42" },
|
{ id: "AL-002", type: "局部高温", robot: "巡检机器人 Q1", project: "青云隧道", status: "待处理", time: "2026-06-23 10:18:42" },
|
||||||
{ id: "AL-003", type: "墙体裂缝", robot: "巡检机器人 N3", project: "南山隧道", status: "处置完成", time: "2026-06-23 09:56:11" },
|
{ id: "AL-003", type: "墙体渗漏", robot: "巡检机器人 N3", project: "南山隧道", status: "处置完成", time: "2026-06-23 09:56:11" },
|
||||||
{ id: "AL-004", type: "水渍渗漏", robot: "巡检机器人 R2", project: "西江隧道", status: "误判", time: "2026-06-23 09:20:36" },
|
{ id: "AL-004", type: "水渍渗漏", robot: "巡检机器人 R2", project: "西江隧道", status: "误判", time: "2026-06-23 09:20:36" },
|
||||||
{ id: "AL-005", type: "应急门开启异常", robot: "巡检机器人 N1", project: "南山隧道", status: "待处理", time: "2026-06-23 08:42:57" },
|
{ id: "AL-005", type: "应急门开启异常", robot: "巡检机器人 N1", project: "南山隧道", status: "待处理", time: "2026-06-23 08:42:57" },
|
||||||
{ id: "AL-006", type: "局部高温", robot: "巡检机器人 N3", project: "南山隧道", status: "处置完成", time: "2026-06-22 19:12:06" },
|
{ id: "AL-006", type: "局部高温", robot: "巡检机器人 N3", project: "南山隧道", status: "处置完成", time: "2026-06-22 19:12:06" },
|
||||||
{ id: "AL-007", type: "线缆破损", robot: "巡检机器人 R1", project: "西江隧道", status: "处置完成", time: "2026-06-22 17:46:28" },
|
{ id: "AL-007", type: "线缆破损", robot: "巡检机器人 R1", project: "西江隧道", status: "处置完成", time: "2026-06-22 17:46:28" },
|
||||||
{ id: "AL-008", type: "墙体裂缝", robot: "巡检机器人 Q2", project: "青云隧道", status: "待研判", time: "2026-06-22 16:08:40" },
|
{ id: "AL-008", type: "墙体渗漏", robot: "巡检机器人 Q2", project: "青云隧道", status: "待研判", time: "2026-06-22 16:08:40" },
|
||||||
{ id: "AL-009", type: "水渍渗漏", robot: "巡检机器人 R4", project: "西江隧道", status: "待处理", time: "2026-06-22 14:21:19" },
|
{ id: "AL-009", type: "水渍渗漏", robot: "巡检机器人 R4", project: "西江隧道", status: "待处理", time: "2026-06-22 14:21:19" },
|
||||||
{ id: "AL-010", type: "局部高温", robot: "巡检机器人 N1", project: "南山隧道", status: "误判", time: "2026-06-22 11:03:50" },
|
{ id: "AL-010", type: "局部高温", robot: "巡检机器人 N1", project: "南山隧道", status: "误判", time: "2026-06-22 11:03:50" },
|
||||||
{ id: "AL-011", type: "线缆破损", robot: "巡检机器人 R3", project: "西江隧道", status: "处置完成", time: "2026-06-16 13:14:09" },
|
{ id: "AL-011", type: "线缆破损", robot: "巡检机器人 R3", project: "西江隧道", status: "处置完成", time: "2026-06-16 13:14:09" },
|
||||||
{ id: "AL-012", type: "局部高温", robot: "巡检机器人 Q1", project: "青云隧道", status: "待处理", time: "2026-06-15 16:42:33" },
|
{ id: "AL-012", type: "局部高温", robot: "巡检机器人 Q1", project: "青云隧道", status: "待处理", time: "2026-06-15 16:42:33" },
|
||||||
{ id: "AL-013", type: "墙体裂缝", robot: "巡检机器人 N2", project: "南山隧道", status: "处置完成", time: "2026-06-12 09:25:50" },
|
{ id: "AL-013", type: "墙体渗漏", robot: "巡检机器人 N2", project: "南山隧道", status: "处置完成", time: "2026-06-12 09:25:50" },
|
||||||
{ id: "AL-014", type: "水渍渗漏", robot: "巡检机器人 R2", project: "西江隧道", status: "误判", time: "2026-06-08 20:14:27" },
|
{ id: "AL-014", type: "水渍渗漏", robot: "巡检机器人 R2", project: "西江隧道", status: "误判", time: "2026-06-08 20:14:27" },
|
||||||
{ id: "AL-015", type: "应急门开启异常", robot: "巡检机器人 Q3", project: "青云隧道", status: "待研判", time: "2026-05-28 11:38:44" },
|
{ id: "AL-015", type: "应急门开启异常", robot: "巡检机器人 Q3", project: "青云隧道", status: "待研判", time: "2026-05-28 11:38:44" },
|
||||||
{ id: "AL-016", type: "局部高温", robot: "巡检机器人 N4", project: "南山隧道", status: "处置完成", time: "2026-05-19 15:07:18" },
|
{ id: "AL-016", type: "局部高温", robot: "巡检机器人 N4", project: "南山隧道", status: "处置完成", time: "2026-05-19 15:07:18" },
|
||||||
{ id: "AL-017", type: "线缆破损", robot: "巡检机器人 R1", project: "西江隧道", status: "待处理", time: "2026-05-06 08:11:52" },
|
{ id: "AL-017", type: "线缆破损", robot: "巡检机器人 R1", project: "西江隧道", status: "待处理", time: "2026-05-06 08:11:52" },
|
||||||
{ id: "AL-018", type: "墙体裂缝", robot: "巡检机器人 Q2", project: "青云隧道", status: "误判", time: "2026-04-23 14:54:06" },
|
{ id: "AL-018", type: "墙体渗漏", robot: "巡检机器人 Q2", project: "青云隧道", status: "误判", time: "2026-04-23 14:54:06" },
|
||||||
{ id: "AL-019", type: "水渍渗漏", robot: "巡检机器人 N1", project: "南山隧道", status: "处置完成", time: "2026-04-12 10:03:16" },
|
{ id: "AL-019", type: "水渍渗漏", robot: "巡检机器人 N1", project: "南山隧道", status: "处置完成", time: "2026-04-12 10:03:16" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
updateRosCarMapUrls,
|
updateRosCarMapUrls,
|
||||||
uploadFile,
|
uploadFile,
|
||||||
} from "@/api/rosCar";
|
} from "@/api/rosCar";
|
||||||
|
import { parseRosMapYaml } from "@/utils/rosMapYaml";
|
||||||
|
|
||||||
type DeviceStatus = "空闲" | "巡检" | "离线" | "充电中";
|
type DeviceStatus = "空闲" | "巡检" | "离线" | "充电中";
|
||||||
type PatrolPointType = "业务点" | "途经点";
|
type PatrolPointType = "业务点" | "途经点";
|
||||||
@@ -222,7 +223,7 @@ const recognitionActionOptions: RecognitionActionOption[] = [
|
|||||||
{ code: "CABLE_SURFACE_DAMAGE", value: "1", label: "电缆表面破损" },
|
{ code: "CABLE_SURFACE_DAMAGE", value: "1", label: "电缆表面破损" },
|
||||||
{ code: "BRIDGE_BREAK", value: "2", label: "桥架断裂" },
|
{ code: "BRIDGE_BREAK", value: "2", label: "桥架断裂" },
|
||||||
{ code: "TUNNEL_WATER", value: "3", label: "隧道积水" },
|
{ code: "TUNNEL_WATER", value: "3", label: "隧道积水" },
|
||||||
{ code: "WALL_CRACK", value: "4", label: "墙体裂缝" },
|
{ code: "WALL_CRACK", value: "4", label: "墙体渗漏" },
|
||||||
];
|
];
|
||||||
const taskNodeTypeMap: Record<PatrolPointType, "via_point" | "action_point"> = {
|
const taskNodeTypeMap: Record<PatrolPointType, "via_point" | "action_point"> = {
|
||||||
业务点: "action_point",
|
业务点: "action_point",
|
||||||
@@ -266,10 +267,12 @@ const formError = ref("");
|
|||||||
const mapEditorMode = ref<EditorMode>("select");
|
const mapEditorMode = ref<EditorMode>("select");
|
||||||
const mapStageRef = ref<HTMLElement | null>(null);
|
const mapStageRef = ref<HTMLElement | null>(null);
|
||||||
const mapCanvasRef = ref<HTMLCanvasElement | null>(null);
|
const mapCanvasRef = ref<HTMLCanvasElement | null>(null);
|
||||||
|
const mapOverviewRef = ref<HTMLCanvasElement | null>(null);
|
||||||
const suppressStageClick = ref(false);
|
const suppressStageClick = ref(false);
|
||||||
const minMapZoom = 0.6;
|
const minMapZoom = 0.6;
|
||||||
const maxMapZoom = 3;
|
const maxMapZoom = 15;
|
||||||
const mapZoomStep = 0.2;
|
const mapOverviewWidth = 200;
|
||||||
|
const mapOverviewHeight = 130;
|
||||||
const mapZoom = ref(1);
|
const mapZoom = ref(1);
|
||||||
const mapZoomPercent = computed(() => Math.round(mapZoom.value * 100));
|
const mapZoomPercent = computed(() => Math.round(mapZoom.value * 100));
|
||||||
|
|
||||||
@@ -337,29 +340,21 @@ const mapPointPalette: Record<
|
|||||||
{
|
{
|
||||||
stroke: string;
|
stroke: string;
|
||||||
glow: string;
|
glow: string;
|
||||||
labelBackground: string;
|
|
||||||
labelBorder: string;
|
|
||||||
}
|
}
|
||||||
> = {
|
> = {
|
||||||
业务点: {
|
业务点: {
|
||||||
stroke: "#66d7ff",
|
stroke: "#66d7ff",
|
||||||
glow: "rgba(102, 215, 255, 0.28)",
|
glow: "rgba(102, 215, 255, 0.28)",
|
||||||
labelBackground: "rgba(16, 74, 118, 0.92)",
|
|
||||||
labelBorder: "rgba(91, 208, 255, 0.18)",
|
|
||||||
},
|
},
|
||||||
途经点: {
|
途经点: {
|
||||||
stroke: "#9af7c8",
|
stroke: "#9af7c8",
|
||||||
glow: "rgba(154, 247, 200, 0.3)",
|
glow: "rgba(154, 247, 200, 0.3)",
|
||||||
labelBackground: "rgba(18, 90, 66, 0.94)",
|
|
||||||
labelBorder: "rgba(91, 208, 255, 0.18)",
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const selectedPointHighlight = {
|
const selectedPointHighlight = {
|
||||||
stroke: "#ffb26e",
|
stroke: "#ffb26e",
|
||||||
glow: "rgba(255, 178, 110, 0.32)",
|
glow: "rgba(255, 178, 110, 0.32)",
|
||||||
labelBackground: "rgba(122, 72, 18, 0.94)",
|
|
||||||
labelBorder: "rgba(255, 196, 118, 0.72)",
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultRosMapMeta: RosMapMetaState = {
|
const defaultRosMapMeta: RosMapMetaState = {
|
||||||
@@ -381,6 +376,10 @@ const mapConfigInputRef = ref<HTMLInputElement | null>(null);
|
|||||||
rosMapImage.onload = () => {
|
rosMapImage.onload = () => {
|
||||||
rosMapMeta.width = rosMapImage.naturalWidth || rosMapMeta.width;
|
rosMapMeta.width = rosMapImage.naturalWidth || rosMapMeta.width;
|
||||||
rosMapMeta.height = rosMapImage.naturalHeight || rosMapMeta.height;
|
rosMapMeta.height = rosMapImage.naturalHeight || rosMapMeta.height;
|
||||||
|
// 图片尺寸就绪后按实际宽高比重算默认缩放,避免宽扁地图默认显示过小
|
||||||
|
if (configDialogVisible.value) {
|
||||||
|
resetMapView();
|
||||||
|
}
|
||||||
scheduleMapCanvasRender();
|
scheduleMapCanvasRender();
|
||||||
};
|
};
|
||||||
rosMapImage.src = rosMapMeta.imageUrl;
|
rosMapImage.src = rosMapMeta.imageUrl;
|
||||||
@@ -436,27 +435,6 @@ function loadRosMapPreview(previewUrl: string, imageName: string) {
|
|||||||
scheduleMapCanvasRender();
|
scheduleMapCanvasRender();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 解析 ROS YAML 配置中的 image、resolution 和 origin 字段
|
|
||||||
function parseRosMapYaml(content: string) {
|
|
||||||
const image = content.match(/^\s*image\s*:\s*(.+?)\s*$/m)?.[1]?.replace(/^['"]|['"]$/g, "");
|
|
||||||
const resolution = Number(content.match(/^\s*resolution\s*:\s*([^\s#]+)\s*$/m)?.[1]);
|
|
||||||
const originValue = content.match(/^\s*origin\s*:\s*\[([^\]]+)\]\s*$/m)?.[1];
|
|
||||||
const origin = originValue
|
|
||||||
?.split(",")
|
|
||||||
.map((value) => Number(value.trim()))
|
|
||||||
.slice(0, 3);
|
|
||||||
|
|
||||||
return {
|
|
||||||
image,
|
|
||||||
resolution: Number.isFinite(resolution) && resolution > 0
|
|
||||||
? resolution
|
|
||||||
: null,
|
|
||||||
origin: origin?.length === 3 && origin.every((value) => Number.isFinite(value))
|
|
||||||
? origin as [number, number, number]
|
|
||||||
: null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function readFileAsArrayBuffer(file: File) {
|
function readFileAsArrayBuffer(file: File) {
|
||||||
return new Promise<ArrayBuffer>((resolve, reject) => {
|
return new Promise<ArrayBuffer>((resolve, reject) => {
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
@@ -582,7 +560,7 @@ function markCurrentMapUploaded(payload: Partial<MapRecord>) {
|
|||||||
currentConfig.value.lastSyncedAt = nowString();
|
currentConfig.value.lastSyncedAt = nowString();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 调用小车地图地址接口,在保存下发时单独保存 PGM 和 YAML 文件 URL
|
// 调用小车地图地址接口,在保存时单独保存 PGM 和 YAML 文件 URL
|
||||||
async function saveCurrentMapUrls() {
|
async function saveCurrentMapUrls() {
|
||||||
if (!currentConfiguredDevice.value || !currentConfig.value) {
|
if (!currentConfiguredDevice.value || !currentConfig.value) {
|
||||||
return;
|
return;
|
||||||
@@ -742,7 +720,7 @@ async function handleMapImageFileChange(event: Event) {
|
|||||||
|
|
||||||
rosMapMeta.imageUrl = uploadedUrl;
|
rosMapMeta.imageUrl = uploadedUrl;
|
||||||
markCurrentMapUploaded({ imageUrl: uploadedUrl, name: file.name });
|
markCurrentMapUploaded({ imageUrl: uploadedUrl, name: file.name });
|
||||||
mapUploadMessage.value = "地图 PGM 上传成功,保存下发后生效";
|
mapUploadMessage.value = "地图 PGM 上传成功,保存后生效";
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
mapUploadMessage.value = "地图 PGM 解析或上传失败,请确认文件为 P5 格式";
|
mapUploadMessage.value = "地图 PGM 解析或上传失败,请确认文件为 P5 格式";
|
||||||
@@ -794,7 +772,7 @@ async function handleMapConfigFileChange(event: Event) {
|
|||||||
|
|
||||||
rosMapMeta.configUrl = uploadedUrl;
|
rosMapMeta.configUrl = uploadedUrl;
|
||||||
markCurrentMapUploaded({ configUrl: uploadedUrl });
|
markCurrentMapUploaded({ configUrl: uploadedUrl });
|
||||||
mapUploadMessage.value = "地图配置上传成功,保存下发后生效";
|
mapUploadMessage.value = "地图配置上传成功,保存后生效";
|
||||||
scheduleMapCanvasRender();
|
scheduleMapCanvasRender();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
@@ -1342,7 +1320,6 @@ async function openConfigDialog(device: DeviceRecord) {
|
|||||||
configuringDeviceId.value = device.id;
|
configuringDeviceId.value = device.id;
|
||||||
configDialogVisible.value = true;
|
configDialogVisible.value = true;
|
||||||
mapEditorMode.value = "normal";
|
mapEditorMode.value = "normal";
|
||||||
resetMapView();
|
|
||||||
|
|
||||||
if (!config.plans.some((plan) => plan.id === config.activePlanId)) {
|
if (!config.plans.some((plan) => plan.id === config.activePlanId)) {
|
||||||
config.activePlanId = config.plans[0]?.id ?? "";
|
config.activePlanId = config.plans[0]?.id ?? "";
|
||||||
@@ -1366,6 +1343,7 @@ async function openConfigDialog(device: DeviceRecord) {
|
|||||||
resetPlanForm();
|
resetPlanForm();
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
attachMapResizeObserver();
|
attachMapResizeObserver();
|
||||||
|
resetMapView();
|
||||||
scheduleMapCanvasRender();
|
scheduleMapCanvasRender();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1532,33 +1510,105 @@ function clampMapZoom(value: number) {
|
|||||||
return Math.min(maxMapZoom, Math.max(minMapZoom, Number(value.toFixed(2))));
|
return Math.min(maxMapZoom, Math.max(minMapZoom, Number(value.toFixed(2))));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置地图缩放比例并触发画布重绘
|
// 设置地图缩放比例并触发画布重绘;传入 focusPoint(画布内坐标)时缩放保持该焦点下的地图点不动
|
||||||
function setMapZoom(value: number) {
|
function setMapZoom(value: number, focusPoint?: { x: number; y: number }) {
|
||||||
mapZoom.value = clampMapZoom(value);
|
const nextZoom = clampMapZoom(value);
|
||||||
|
|
||||||
|
if (focusPoint && nextZoom !== mapZoom.value) {
|
||||||
|
const rect = getMapStageRect();
|
||||||
|
|
||||||
|
if (rect && rect.width > 0 && rect.height > 0) {
|
||||||
|
const frame = getRosMapFrame(rect);
|
||||||
|
const pointerRatioX = (focusPoint.x - frame.left) / frame.width;
|
||||||
|
const pointerRatioY = (focusPoint.y - frame.top) / frame.height;
|
||||||
|
const nextWidth = frame.width * (nextZoom / mapZoom.value);
|
||||||
|
const nextHeight = frame.height * (nextZoom / mapZoom.value);
|
||||||
|
|
||||||
|
mapPan.x += (frame.width - nextWidth) * (pointerRatioX - 0.5);
|
||||||
|
mapPan.y += (frame.height - nextHeight) * (pointerRatioY - 0.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mapZoom.value = nextZoom;
|
||||||
scheduleMapCanvasRender();
|
scheduleMapCanvasRender();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 按固定步长放大地图
|
// 按固定倍率放大地图,倍率越高放大步进越大
|
||||||
function zoomInMap() {
|
function zoomInMap() {
|
||||||
setMapZoom(mapZoom.value + mapZoomStep);
|
setMapZoom(mapZoom.value * 1.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 按固定步长缩小地图
|
// 按固定倍率缩小地图
|
||||||
function zoomOutMap() {
|
function zoomOutMap() {
|
||||||
setMapZoom(mapZoom.value - mapZoomStep);
|
setMapZoom(mapZoom.value / 1.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 将地图缩放比例和平移位置恢复为默认状态
|
// 将地图缩放比例和平移位置恢复为默认状态:宽高比自适应缩放并聚焦原点
|
||||||
function resetMapView() {
|
function resetMapView() {
|
||||||
mapPan.x = 0;
|
mapPan.x = 0;
|
||||||
mapPan.y = 0;
|
mapPan.y = 0;
|
||||||
setMapZoom(1);
|
setMapZoom(computeDefaultMapZoom());
|
||||||
|
focusMapOnOrigin();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理鼠标滚轮缩放地图
|
// 处理鼠标滚轮缩放地图,以光标位置为缩放中心
|
||||||
function handleMapWheel(event: WheelEvent) {
|
function handleMapWheel(event: WheelEvent) {
|
||||||
const zoomDirection = event.deltaY > 0 ? -1 : 1;
|
const rect = getMapStageRect();
|
||||||
setMapZoom(mapZoom.value + zoomDirection * mapZoomStep);
|
const zoomFactor = event.deltaY > 0 ? 1 / 1.1 : 1.1;
|
||||||
|
const focusPoint = rect ? { x: event.clientX - rect.left, y: event.clientY - rect.top } : undefined;
|
||||||
|
setMapZoom(mapZoom.value * zoomFactor, focusPoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据地图宽高比计算默认缩放:常规比例地图保持完整显示,宽扁/高瘦地图放大让短边占满画布
|
||||||
|
function computeDefaultMapZoom() {
|
||||||
|
const rect = getMapStageRect();
|
||||||
|
|
||||||
|
if (!rect || rect.width <= 0 || rect.height <= 0) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const imageRatio = rosMapImage.naturalWidth && rosMapImage.naturalHeight
|
||||||
|
? rosMapImage.naturalWidth / rosMapImage.naturalHeight
|
||||||
|
: 178 / 50;
|
||||||
|
const padding = 30;
|
||||||
|
const maxWidth = rect.width - padding * 2;
|
||||||
|
const maxHeight = rect.height - padding * 2;
|
||||||
|
|
||||||
|
if (maxWidth <= 0 || maxHeight <= 0) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 与 getRosMapFrame 的 fit 逻辑保持一致:完整显示时短边占比过小才放大到铺满画布
|
||||||
|
const fitHeight = maxWidth / imageRatio;
|
||||||
|
const fitWidth = maxHeight * imageRatio;
|
||||||
|
const shortSideRatio = Math.min(fitWidth / maxWidth, fitHeight / maxHeight);
|
||||||
|
|
||||||
|
if (shortSideRatio >= 0.6) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultZoom = fitHeight <= maxHeight
|
||||||
|
? maxHeight / fitHeight
|
||||||
|
: maxWidth / fitWidth;
|
||||||
|
|
||||||
|
return Number(defaultZoom.toFixed(2));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将地图视野平移到原点位置,使原点位于画布中心
|
||||||
|
function focusMapOnOrigin() {
|
||||||
|
const rect = getMapStageRect();
|
||||||
|
|
||||||
|
if (!rect || rect.width <= 0 || rect.height <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const originRatioX = (0 - rosMapMeta.origin[0]) / rosMapMeta.resolution / rosMapMeta.width;
|
||||||
|
const originRatioY = 1 - (0 - rosMapMeta.origin[1]) / rosMapMeta.resolution / rosMapMeta.height;
|
||||||
|
const frame = getRosMapFrame(rect);
|
||||||
|
|
||||||
|
// 原点位于画布中心时,平移量等于地图尺寸的一半减去原点偏移
|
||||||
|
mapPan.x = frame.width * (0.5 - originRatioX);
|
||||||
|
mapPan.y = frame.height * (0.5 - originRatioY);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 根据 ROS 地图图片比例计算画布中的实际地图显示区域
|
// 根据 ROS 地图图片比例计算画布中的实际地图显示区域
|
||||||
@@ -1597,28 +1647,6 @@ function percentToStagePixels(x: number, y: number, rect: DOMRect) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function estimatePointLabelWidth(name: string) {
|
|
||||||
return Math.max(72, name.length * 12 + 18);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getPointLabelFrame(point: PatrolPoint, rect: DOMRect) {
|
|
||||||
const pixelPoint = percentToStagePixels(point.x, point.y, rect);
|
|
||||||
const labelWidth = estimatePointLabelWidth(point.name);
|
|
||||||
const labelHeight = 24;
|
|
||||||
let left = pixelPoint.x + 18;
|
|
||||||
|
|
||||||
if (left + labelWidth > rect.width - 10) {
|
|
||||||
left = pixelPoint.x - labelWidth - 18;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
left,
|
|
||||||
top: pixelPoint.y - labelHeight / 2,
|
|
||||||
width: labelWidth,
|
|
||||||
height: labelHeight,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function findHitPoint(event: MouseEvent) {
|
function findHitPoint(event: MouseEvent) {
|
||||||
const rect = getMapStageRect();
|
const rect = getMapStageRect();
|
||||||
if (!rect) {
|
if (!rect) {
|
||||||
@@ -1637,45 +1665,11 @@ function findHitPoint(event: MouseEvent) {
|
|||||||
if (Math.hypot(deltaX, deltaY) <= 18) {
|
if (Math.hypot(deltaX, deltaY) <= 18) {
|
||||||
return point;
|
return point;
|
||||||
}
|
}
|
||||||
|
|
||||||
const labelFrame = getPointLabelFrame(point, rect);
|
|
||||||
|
|
||||||
if (
|
|
||||||
pointerX >= labelFrame.left &&
|
|
||||||
pointerX <= labelFrame.left + labelFrame.width &&
|
|
||||||
pointerY >= labelFrame.top &&
|
|
||||||
pointerY <= labelFrame.top + labelFrame.height
|
|
||||||
) {
|
|
||||||
return point;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function drawRoundRect(
|
|
||||||
ctx: CanvasRenderingContext2D,
|
|
||||||
x: number,
|
|
||||||
y: number,
|
|
||||||
width: number,
|
|
||||||
height: number,
|
|
||||||
radius: number,
|
|
||||||
) {
|
|
||||||
const clampedRadius = Math.min(radius, width / 2, height / 2);
|
|
||||||
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.moveTo(x + clampedRadius, y);
|
|
||||||
ctx.lineTo(x + width - clampedRadius, y);
|
|
||||||
ctx.quadraticCurveTo(x + width, y, x + width, y + clampedRadius);
|
|
||||||
ctx.lineTo(x + width, y + height - clampedRadius);
|
|
||||||
ctx.quadraticCurveTo(x + width, y + height, x + width - clampedRadius, y + height);
|
|
||||||
ctx.lineTo(x + clampedRadius, y + height);
|
|
||||||
ctx.quadraticCurveTo(x, y + height, x, y + height - clampedRadius);
|
|
||||||
ctx.lineTo(x, y + clampedRadius);
|
|
||||||
ctx.quadraticCurveTo(x, y, x + clampedRadius, y);
|
|
||||||
ctx.closePath();
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderMapCanvas() {
|
function renderMapCanvas() {
|
||||||
const canvas = mapCanvasRef.value;
|
const canvas = mapCanvasRef.value;
|
||||||
const rect = getMapStageRect();
|
const rect = getMapStageRect();
|
||||||
@@ -1790,17 +1784,6 @@ function renderMapCanvas() {
|
|||||||
context.beginPath();
|
context.beginPath();
|
||||||
context.arc(originPoint.x, originPoint.y, 4, 0, Math.PI * 2);
|
context.arc(originPoint.x, originPoint.y, 4, 0, Math.PI * 2);
|
||||||
context.fill();
|
context.fill();
|
||||||
drawRoundRect(context, originPoint.x + 12, originPoint.y - 16, 126, 32, 8);
|
|
||||||
context.fillStyle = "rgba(72, 42, 13, 0.92)";
|
|
||||||
context.strokeStyle = "rgba(255, 196, 118, 0.72)";
|
|
||||||
context.lineWidth = 1;
|
|
||||||
context.fill();
|
|
||||||
context.stroke();
|
|
||||||
context.fillStyle = "#fff5df";
|
|
||||||
context.font = '600 12px "Microsoft YaHei", sans-serif';
|
|
||||||
context.textAlign = "left";
|
|
||||||
context.textBaseline = "middle";
|
|
||||||
context.fillText("原点 (0.00, 0.00)", originPoint.x + 22, originPoint.y);
|
|
||||||
context.restore();
|
context.restore();
|
||||||
|
|
||||||
if (activeRoundPoints.value.length > 1) {
|
if (activeRoundPoints.value.length > 1) {
|
||||||
@@ -1833,44 +1816,28 @@ function renderMapCanvas() {
|
|||||||
const palette = mapPointPalette[point.type];
|
const palette = mapPointPalette[point.type];
|
||||||
const selected = currentConfig.value?.selectedPointIds.includes(point.id) ?? false;
|
const selected = currentConfig.value?.selectedPointIds.includes(point.id) ?? false;
|
||||||
const highlightPalette = selected ? selectedPointHighlight : palette;
|
const highlightPalette = selected ? selectedPointHighlight : palette;
|
||||||
const labelFrame = getPointLabelFrame(point, rect);
|
|
||||||
|
|
||||||
context.save();
|
context.save();
|
||||||
context.fillStyle = selected ? highlightPalette.glow : "rgba(255, 255, 255, 0.02)";
|
context.fillStyle = selected ? highlightPalette.glow : "rgba(255, 255, 255, 0.02)";
|
||||||
context.beginPath();
|
context.beginPath();
|
||||||
context.arc(pixelPoint.x, pixelPoint.y, selected ? 18 : 14, 0, Math.PI * 2);
|
context.arc(pixelPoint.x, pixelPoint.y, selected ? 14 : 10, 0, Math.PI * 2);
|
||||||
context.fill();
|
context.fill();
|
||||||
|
|
||||||
context.fillStyle = "rgba(5, 18, 38, 0.94)";
|
context.fillStyle = "rgba(5, 18, 38, 0.94)";
|
||||||
context.strokeStyle = highlightPalette.stroke;
|
context.strokeStyle = highlightPalette.stroke;
|
||||||
context.lineWidth = selected ? 3 : 2;
|
context.lineWidth = selected ? 3 : 2;
|
||||||
context.beginPath();
|
context.beginPath();
|
||||||
context.arc(pixelPoint.x, pixelPoint.y, 11, 0, Math.PI * 2);
|
context.arc(pixelPoint.x, pixelPoint.y, 8, 0, Math.PI * 2);
|
||||||
context.fill();
|
context.fill();
|
||||||
context.shadowColor = highlightPalette.glow;
|
context.shadowColor = highlightPalette.glow;
|
||||||
context.shadowBlur = selected ? 18 : 12;
|
context.shadowBlur = selected ? 18 : 12;
|
||||||
context.stroke();
|
context.stroke();
|
||||||
|
|
||||||
drawRoundRect(context, labelFrame.left, labelFrame.top, labelFrame.width, labelFrame.height, 8);
|
// 所有点位统一圆圈大小,仅以描边颜色区分类型,内部标注序号
|
||||||
context.fillStyle = selected
|
|
||||||
? highlightPalette.labelBackground
|
|
||||||
: "rgba(4, 19, 39, 0.82)";
|
|
||||||
context.strokeStyle = selected
|
|
||||||
? highlightPalette.labelBorder
|
|
||||||
: "rgba(91, 208, 255, 0.18)";
|
|
||||||
context.lineWidth = 1;
|
|
||||||
context.shadowBlur = 0;
|
|
||||||
context.fill();
|
|
||||||
context.stroke();
|
|
||||||
|
|
||||||
context.fillStyle = "#eefcff";
|
context.fillStyle = "#eefcff";
|
||||||
context.font = '700 11px "Microsoft YaHei", sans-serif';
|
context.font = '700 10px "Microsoft YaHei", sans-serif';
|
||||||
context.textAlign = "center";
|
context.textAlign = "center";
|
||||||
context.fillText(String(index + 1), pixelPoint.x, pixelPoint.y + 0.5);
|
context.fillText(String(index + 1), pixelPoint.x, pixelPoint.y + 0.5);
|
||||||
|
|
||||||
context.font = '500 12px "Microsoft YaHei", sans-serif';
|
|
||||||
context.textAlign = "left";
|
|
||||||
context.fillText(point.name, labelFrame.left + 8, labelFrame.top + labelFrame.height / 2 + 0.5);
|
|
||||||
context.restore();
|
context.restore();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1889,6 +1856,123 @@ function renderMapCanvas() {
|
|||||||
context.strokeRect(left, top, width, height);
|
context.strokeRect(left, top, width, height);
|
||||||
context.restore();
|
context.restore();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
drawMapOverview();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 全局缩略图预览区中整张地图的实际绘制区域(contain 等比缩放)
|
||||||
|
function getMapOverviewDrawArea() {
|
||||||
|
const imageRatio = rosMapImage.naturalWidth && rosMapImage.naturalHeight
|
||||||
|
? rosMapImage.naturalWidth / rosMapImage.naturalHeight
|
||||||
|
: 178 / 50;
|
||||||
|
const drawWidth = Math.min(mapOverviewWidth, mapOverviewHeight * imageRatio);
|
||||||
|
const drawHeight = drawWidth / imageRatio;
|
||||||
|
|
||||||
|
return {
|
||||||
|
drawWidth,
|
||||||
|
drawHeight,
|
||||||
|
offsetX: (mapOverviewWidth - drawWidth) / 2,
|
||||||
|
offsetY: (mapOverviewHeight - drawHeight) / 2,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 在右下角绘制全局缩略图,并标出当前视野在全局地图中的范围
|
||||||
|
function drawMapOverview() {
|
||||||
|
const canvas = mapOverviewRef.value;
|
||||||
|
const rect = getMapStageRect();
|
||||||
|
|
||||||
|
if (!canvas || !rect || rect.width <= 0 || rect.height <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dpr = window.devicePixelRatio || 1;
|
||||||
|
|
||||||
|
if (
|
||||||
|
canvas.width !== Math.round(mapOverviewWidth * dpr) ||
|
||||||
|
canvas.height !== Math.round(mapOverviewHeight * dpr)
|
||||||
|
) {
|
||||||
|
canvas.width = Math.round(mapOverviewWidth * dpr);
|
||||||
|
canvas.height = Math.round(mapOverviewHeight * dpr);
|
||||||
|
}
|
||||||
|
|
||||||
|
const context = canvas.getContext("2d");
|
||||||
|
if (!context) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
context.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||||
|
context.clearRect(0, 0, mapOverviewWidth, mapOverviewHeight);
|
||||||
|
|
||||||
|
if (!rosMapImage.complete || !rosMapImage.naturalWidth) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { drawWidth, drawHeight, offsetX, offsetY } = getMapOverviewDrawArea();
|
||||||
|
|
||||||
|
context.globalAlpha = 0.92;
|
||||||
|
context.drawImage(rosMapImage, offsetX, offsetY, drawWidth, drawHeight);
|
||||||
|
context.globalAlpha = 1;
|
||||||
|
|
||||||
|
// 当前视野对应到全局地图的比例范围
|
||||||
|
const frame = getRosMapFrame(rect);
|
||||||
|
const leftRatio = Math.max(0, -frame.left) / frame.width;
|
||||||
|
const topRatio = Math.max(0, -frame.top) / frame.height;
|
||||||
|
const rightRatio = Math.min(rect.width - frame.left, frame.width) / frame.width;
|
||||||
|
const bottomRatio = Math.min(rect.height - frame.top, frame.height) / frame.height;
|
||||||
|
const viewX = offsetX + leftRatio * drawWidth;
|
||||||
|
const viewY = offsetY + topRatio * drawHeight;
|
||||||
|
const viewWidth = (rightRatio - leftRatio) * drawWidth;
|
||||||
|
const viewHeight = (bottomRatio - topRatio) * drawHeight;
|
||||||
|
|
||||||
|
context.strokeStyle = "rgba(121, 234, 255, 0.95)";
|
||||||
|
context.lineWidth = 1.2;
|
||||||
|
context.strokeRect(viewX, viewY, viewWidth, viewHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mapOverviewDragging = false;
|
||||||
|
|
||||||
|
// 缩略图点击或拖动:将大地图视野中心切换到预览上对应的比例位置
|
||||||
|
function jumpMapOverview(event: PointerEvent) {
|
||||||
|
const canvas = mapOverviewRef.value;
|
||||||
|
const rect = getMapStageRect();
|
||||||
|
|
||||||
|
if (!canvas || !rect || rect.width <= 0 || rect.height <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const overviewRect = canvas.getBoundingClientRect();
|
||||||
|
const { drawWidth, drawHeight, offsetX, offsetY } = getMapOverviewDrawArea();
|
||||||
|
const ratioX = Math.min(1, Math.max(0, (event.clientX - overviewRect.left - offsetX) / drawWidth));
|
||||||
|
const ratioY = Math.min(1, Math.max(0, (event.clientY - overviewRect.top - offsetY) / drawHeight));
|
||||||
|
|
||||||
|
// 平移使该比例位置位于主画布中心
|
||||||
|
const frame = getRosMapFrame(rect);
|
||||||
|
mapPan.x = frame.width * (0.5 - ratioX);
|
||||||
|
mapPan.y = frame.height * (0.5 - ratioY);
|
||||||
|
scheduleMapCanvasRender();
|
||||||
|
}
|
||||||
|
|
||||||
|
function startMapOverviewDrag(event: PointerEvent) {
|
||||||
|
mapOverviewDragging = true;
|
||||||
|
(event.currentTarget as HTMLElement).setPointerCapture?.(event.pointerId);
|
||||||
|
jumpMapOverview(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveMapOverviewDrag(event: PointerEvent) {
|
||||||
|
if (!mapOverviewDragging) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
jumpMapOverview(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
function endMapOverviewDrag(event: PointerEvent) {
|
||||||
|
mapOverviewDragging = false;
|
||||||
|
const target = event.currentTarget as HTMLElement;
|
||||||
|
|
||||||
|
if (target.hasPointerCapture?.(event.pointerId)) {
|
||||||
|
target.releasePointerCapture(event.pointerId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function scheduleMapCanvasRender() {
|
function scheduleMapCanvasRender() {
|
||||||
@@ -2482,14 +2566,8 @@ async function saveConfigDialog() {
|
|||||||
buildInspectionTaskPayload(currentConfiguredDevice.value, currentConfig.value),
|
buildInspectionTaskPayload(currentConfiguredDevice.value, currentConfig.value),
|
||||||
);
|
);
|
||||||
|
|
||||||
saveProgressTimer = window.setInterval(() => {
|
// 保存接口真实调用完成后,进度条立即走完,不再模拟递增动画
|
||||||
const step = saveProgressValue.value < 72 ? 12 : saveProgressValue.value < 90 ? 6 : 3;
|
finishSaveProgress();
|
||||||
saveProgressValue.value = Math.min(100, saveProgressValue.value + step);
|
|
||||||
|
|
||||||
if (saveProgressValue.value >= 100) {
|
|
||||||
finishSaveProgress();
|
|
||||||
}
|
|
||||||
}, 180);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
stopSaveProgressTimers();
|
stopSaveProgressTimers();
|
||||||
@@ -2881,7 +2959,7 @@ watch(
|
|||||||
</button>
|
</button>
|
||||||
<button class="page-action-btn primary" :disabled="saveProgressVisible" type="button"
|
<button class="page-action-btn primary" :disabled="saveProgressVisible" type="button"
|
||||||
@click="saveConfigDialog">
|
@click="saveConfigDialog">
|
||||||
{{ saveProgressVisible ? "保存中..." : "保存下发" }}
|
{{ saveProgressVisible ? "保存中..." : "保存" }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -2952,6 +3030,12 @@ watch(
|
|||||||
<button type="button" @click="zoomInMap">+</button>
|
<button type="button" @click="zoomInMap">+</button>
|
||||||
<button type="button" @click="resetMapView">重置</button>
|
<button type="button" @click="resetMapView">重置</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="map-overview" title="全局地图预览(点击或拖动切换视野)"
|
||||||
|
@pointerdown="startMapOverviewDrag" @pointermove="moveMapOverviewDrag" @pointerup="endMapOverviewDrag"
|
||||||
|
@pointercancel="endMapOverviewDrag">
|
||||||
|
<canvas ref="mapOverviewRef" class="map-overview-canvas"></canvas>
|
||||||
|
<div class="map-overview-label">全局视图</div>
|
||||||
|
</div>
|
||||||
<div class="stage-overlay top-right">
|
<div class="stage-overlay top-right">
|
||||||
{{
|
{{
|
||||||
currentConfig.mapLoaded
|
currentConfig.mapLoaded
|
||||||
@@ -3108,7 +3192,7 @@ watch(
|
|||||||
|
|
||||||
<div v-if="saveProgressVisible" class="save-progress-mask">
|
<div v-if="saveProgressVisible" class="save-progress-mask">
|
||||||
<div class="save-progress-card">
|
<div class="save-progress-card">
|
||||||
<div class="save-progress-title">保存下发</div>
|
<div class="save-progress-title">保存</div>
|
||||||
<div class="save-progress-text">{{ saveProgressMessage }}</div>
|
<div class="save-progress-text">{{ saveProgressMessage }}</div>
|
||||||
<div class="save-progress-track">
|
<div class="save-progress-track">
|
||||||
<div class="save-progress-fill" :style="{ width: `${saveProgressValue}%` }"></div>
|
<div class="save-progress-fill" :style="{ width: `${saveProgressValue}%` }"></div>
|
||||||
@@ -3508,6 +3592,39 @@ watch(
|
|||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.map-overview {
|
||||||
|
position: absolute;
|
||||||
|
right: 14px;
|
||||||
|
bottom: 14px;
|
||||||
|
z-index: 6;
|
||||||
|
padding: 6px;
|
||||||
|
border: 1px solid rgba(86, 205, 255, 0.2);
|
||||||
|
background: rgba(4, 18, 38, 0.78);
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 0 0 16px rgba(0, 10, 24, 0.28);
|
||||||
|
backdrop-filter: blur(2px);
|
||||||
|
cursor: grab;
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-overview:active {
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-overview-canvas {
|
||||||
|
display: block;
|
||||||
|
width: 200px;
|
||||||
|
height: 130px;
|
||||||
|
border: 1px solid rgba(91, 208, 255, 0.14);
|
||||||
|
background: rgba(2, 8, 18, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-overview-label {
|
||||||
|
margin-top: 4px;
|
||||||
|
text-align: center;
|
||||||
|
color: rgba(184, 235, 255, 0.72);
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
.stage-overlay.bottom-left {
|
.stage-overlay.bottom-left {
|
||||||
left: 14px;
|
left: 14px;
|
||||||
bottom: 14px;
|
bottom: 14px;
|
||||||
@@ -3820,7 +3937,7 @@ watch(
|
|||||||
rgba(24, 102, 180, 0.92) 0%,
|
rgba(24, 102, 180, 0.92) 0%,
|
||||||
rgba(35, 158, 215, 0.9) 100%);
|
rgba(35, 158, 215, 0.9) 100%);
|
||||||
box-shadow: 0 0 18px rgba(52, 177, 255, 0.26);
|
box-shadow: 0 0 18px rgba(52, 177, 255, 0.26);
|
||||||
transition: width 0.18s ease;
|
transition: width 0.45s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.save-progress-value {
|
.save-progress-value {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
stopDispatchRosInspectionTask,
|
stopDispatchRosInspectionTask,
|
||||||
} from "@/api/rosCar";
|
} from "@/api/rosCar";
|
||||||
import { getToken } from "@/utils/auth";
|
import { getToken } from "@/utils/auth";
|
||||||
|
import { parseRosMapYaml } from "@/utils/rosMapYaml";
|
||||||
import { BASE_URL } from "@/utils/request";
|
import { BASE_URL } from "@/utils/request";
|
||||||
import { getRecentAlertPhoto } from "../shared/alertPhotos";
|
import { getRecentAlertPhoto } from "../shared/alertPhotos";
|
||||||
|
|
||||||
@@ -376,23 +377,6 @@ function buildFileAccessUrl(fileUrl: string) {
|
|||||||
return `${BASE_URL}${fileUrl.startsWith("/") ? fileUrl : `/${fileUrl}`}`;
|
return `${BASE_URL}${fileUrl.startsWith("/") ? fileUrl : `/${fileUrl}`}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 解析 ROS 地图 YAML 中的分辨率和原点,供真实坐标映射使用
|
|
||||||
function parseRosMapYaml(content: string): RosMapMeta {
|
|
||||||
const resolution = Number(content.match(/^\s*resolution\s*:\s*([^\s#]+)\s*$/m)?.[1]);
|
|
||||||
const originValue = content.match(/^\s*origin\s*:\s*\[([^\]]+)\]\s*$/m)?.[1];
|
|
||||||
const origin = originValue
|
|
||||||
?.split(",")
|
|
||||||
.map((value) => Number(value.trim()))
|
|
||||||
.slice(0, 3);
|
|
||||||
|
|
||||||
return {
|
|
||||||
resolution: Number.isFinite(resolution) && resolution > 0 ? resolution : 0.05,
|
|
||||||
origin: origin?.length === 3 && origin.every((value) => Number.isFinite(value))
|
|
||||||
? origin as [number, number, number]
|
|
||||||
: [0, 0, 0],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function readPgmToken(bytes: Uint8Array, cursor: { index: number }) {
|
function readPgmToken(bytes: Uint8Array, cursor: { index: number }) {
|
||||||
while (cursor.index < bytes.length) {
|
while (cursor.index < bytes.length) {
|
||||||
const code = bytes[cursor.index];
|
const code = bytes[cursor.index];
|
||||||
@@ -484,7 +468,7 @@ const recognizeTypeMap: Record<number, string> = {
|
|||||||
1: "电缆表面破损",
|
1: "电缆表面破损",
|
||||||
2: "桥架断裂",
|
2: "桥架断裂",
|
||||||
3: "隧道积水",
|
3: "隧道积水",
|
||||||
4: "墙体裂缝",
|
4: "墙体渗漏",
|
||||||
};
|
};
|
||||||
const recognizeTypeCodeMap = Object.fromEntries(
|
const recognizeTypeCodeMap = Object.fromEntries(
|
||||||
Object.entries(recognizeTypeMap).map(([value, label]) => [label, Number(value)]),
|
Object.entries(recognizeTypeMap).map(([value, label]) => [label, Number(value)]),
|
||||||
@@ -513,7 +497,7 @@ const inspectionActionLoading = ref(false);
|
|||||||
const lastDispatchTime = ref("");
|
const lastDispatchTime = ref("");
|
||||||
const alertLoading = ref(false);
|
const alertLoading = ref(false);
|
||||||
const alertError = ref("");
|
const alertError = ref("");
|
||||||
const alertTabs = ["局部高温", "电缆表面破损", "桥架断裂", "隧道积水", "墙体裂缝"] as const;
|
const alertTabs = ["局部高温", "电缆表面破损", "桥架断裂", "隧道积水", "墙体渗漏"] as const;
|
||||||
const activeAlertTab = ref<(typeof alertTabs)[number]>("局部高温");
|
const activeAlertTab = ref<(typeof alertTabs)[number]>("局部高温");
|
||||||
const activeCameraMode = ref<"white" | "infrared">("white");
|
const activeCameraMode = ref<"white" | "infrared">("white");
|
||||||
const activeDeviceId = ref("");
|
const activeDeviceId = ref("");
|
||||||
@@ -684,7 +668,7 @@ const realRoutePath = computed(() => routeMapPoints.value
|
|||||||
.join(" "));
|
.join(" "));
|
||||||
const realCheckPoints = computed(() => routeMapPoints.value.map((point, index) => ({
|
const realCheckPoints = computed(() => routeMapPoints.value.map((point, index) => ({
|
||||||
...point,
|
...point,
|
||||||
label: index === 0 ? "起点" : index === routeMapPoints.value.length - 1 ? "终点" : "途经点",
|
label: index === 0 ? "起点" : index === routeMapPoints.value.length - 1 ? "终点" : "",
|
||||||
})));
|
})));
|
||||||
const inspectionRunning = computed(() => currentInspection.value?.status === 0);
|
const inspectionRunning = computed(() => currentInspection.value?.status === 0);
|
||||||
const inspectionStatusText = computed(() => inspectionRunning.value ? "巡检中" : "空闲");
|
const inspectionStatusText = computed(() => inspectionRunning.value ? "巡检中" : "空闲");
|
||||||
@@ -737,7 +721,11 @@ async function loadRobotMap(device: DeviceItem) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const styledMap = buildStyledPgmMap(await pgmResponse.arrayBuffer());
|
const styledMap = buildStyledPgmMap(await pgmResponse.arrayBuffer());
|
||||||
const meta = parseRosMapYaml(await yamlResponse.text());
|
const parsedMeta = parseRosMapYaml(await yamlResponse.text());
|
||||||
|
const meta: RosMapMeta = {
|
||||||
|
resolution: parsedMeta.resolution ?? 0.05,
|
||||||
|
origin: parsedMeta.origin ?? [0, 0, 0],
|
||||||
|
};
|
||||||
|
|
||||||
rosMap.value = {
|
rosMap.value = {
|
||||||
...meta,
|
...meta,
|
||||||
@@ -1282,9 +1270,10 @@ function switchDriveMode(mode: DriveMode) {
|
|||||||
activeDriveMode.value = mode;
|
activeDriveMode.value = mode;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 调整真实 ROS 地图整体缩放,不影响底部状态浮层
|
// 调整真实 ROS 地图整体缩放,不影响底部状态浮层,倍率越高放大步进越大
|
||||||
function changeMapScale(delta: number) {
|
function changeMapScale(delta: number) {
|
||||||
mapScale.value = Math.min(3, Math.max(0.55, Number((mapScale.value + delta).toFixed(2))));
|
const factor = delta > 0 ? 1.1 : 1 / 1.1;
|
||||||
|
mapScale.value = Math.min(15, Math.max(0.55, Number((mapScale.value * factor).toFixed(2))));
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetMapScale() {
|
function resetMapScale() {
|
||||||
@@ -1292,10 +1281,34 @@ function resetMapScale() {
|
|||||||
mapOffset.value = { x: 0, y: 0 };
|
mapOffset.value = { x: 0, y: 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
// 鼠标滚轮缩放地图内容,按住拖拽平移地图视野
|
// 鼠标滚轮以光标为缩放中心缩放地图内容,按住拖拽平移地图视野
|
||||||
function handleMapWheel(event: WheelEvent) {
|
function handleMapWheel(event: WheelEvent) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
changeMapScale(event.deltaY > 0 ? -0.05 : 0.05);
|
|
||||||
|
if (!rosMap.value || !navMapSvgRef.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const factor = event.deltaY > 0 ? 1 / 1.1 : 1.1;
|
||||||
|
const nextScale = Math.min(15, Math.max(0.55, Number((mapScale.value * factor).toFixed(2))));
|
||||||
|
|
||||||
|
if (nextScale === mapScale.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 缩放前后保持光标下的地图点位置不动,需要同步平移视野
|
||||||
|
const rect = navMapSvgRef.value.getBoundingClientRect();
|
||||||
|
const pointerX = (event.clientX - rect.left) * (rosMap.value.width / rect.width);
|
||||||
|
const pointerY = (event.clientY - rect.top) * (rosMap.value.height / rect.height);
|
||||||
|
const centerX = rosMap.value.width / 2;
|
||||||
|
const centerY = rosMap.value.height / 2;
|
||||||
|
const keepRatio = 1 - nextScale / mapScale.value;
|
||||||
|
|
||||||
|
mapOffset.value = {
|
||||||
|
x: mapOffset.value.x + (pointerX - centerX) * keepRatio,
|
||||||
|
y: mapOffset.value.y + (pointerY - centerY) * keepRatio,
|
||||||
|
};
|
||||||
|
mapScale.value = nextScale;
|
||||||
}
|
}
|
||||||
|
|
||||||
function startMapDrag(event: PointerEvent) {
|
function startMapDrag(event: PointerEvent) {
|
||||||
@@ -1551,10 +1564,10 @@ function getStatusClass(status: DeviceItem["status"]) {
|
|||||||
<path v-if="realRoutePath" class="route-glow" :d="realRoutePath" />
|
<path v-if="realRoutePath" class="route-glow" :d="realRoutePath" />
|
||||||
<path v-if="realRoutePath" class="route-line" :d="realRoutePath" />
|
<path v-if="realRoutePath" class="route-line" :d="realRoutePath" />
|
||||||
<g class="checkpoint-layer">
|
<g class="checkpoint-layer">
|
||||||
<g v-for="point in realCheckPoints" :key="`${point.label}-${point.x}-${point.y}`" class="checkpoint"
|
<g v-for="(point, index) in realCheckPoints" :key="index" class="checkpoint"
|
||||||
:transform="`translate(${point.x}, ${point.y})`">
|
:transform="`translate(${point.x}, ${point.y})`">
|
||||||
<circle r="3"></circle>
|
<circle r="3"></circle>
|
||||||
<text x="7" y="2.5">{{ point.label }}</text>
|
<text v-if="point.label" x="7" y="2.5">{{ point.label }}</text>
|
||||||
</g>
|
</g>
|
||||||
</g>
|
</g>
|
||||||
<g v-if="robotMapPoint" class="robot-layer real-robot-layer"
|
<g v-if="robotMapPoint" class="robot-layer real-robot-layer"
|
||||||
@@ -1636,14 +1649,15 @@ function getStatusClass(status: DeviceItem["status"]) {
|
|||||||
</div>
|
</div>
|
||||||
<div class="video-panel">
|
<div class="video-panel">
|
||||||
<div class="video-screen">
|
<div class="video-screen">
|
||||||
|
<!-- <img src="/image.png" alt="" class="monitor-video"> -->
|
||||||
<video v-if="currentMonitorVideo" ref="monitorVideoRef" :key="currentMonitorVideo"
|
<video v-if="currentMonitorVideo" ref="monitorVideoRef" :key="currentMonitorVideo"
|
||||||
:class="['monitor-video', { infrared: activeCameraMode === 'infrared' }]" autoplay muted
|
:class="['monitor-video', { infrared: activeCameraMode === 'infrared' }]" autoplay muted
|
||||||
playsinline></video>
|
playsinline></video>
|
||||||
<div class="scan-line"></div>
|
<div class="scan-line"></div>
|
||||||
<!-- <div class="video-overlay top-left">实时视频流</div>
|
<div class="video-overlay top-left">实时视频流</div>
|
||||||
<div class="video-overlay top-right">
|
<div class="video-overlay top-right">
|
||||||
{{ activeCameraMode === "white" ? "白光镜头" : "红外镜头" }}
|
{{ activeCameraMode === "white" ? "白光镜头" : "红外镜头" }}
|
||||||
</div> -->
|
</div>
|
||||||
<div class="video-overlay bottom-left">{{ currentRobot.id }}</div>
|
<div class="video-overlay bottom-left">{{ currentRobot.id }}</div>
|
||||||
<div v-if="streamLoading" class="video-empty">视频流连接中...</div>
|
<div v-if="streamLoading" class="video-empty">视频流连接中...</div>
|
||||||
<div v-else-if="streamError" class="video-empty">{{ streamError }}</div>
|
<div v-else-if="streamError" class="video-empty">{{ streamError }}</div>
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ const patrolRecords: PatrolRecord[] = [
|
|||||||
startTime: "2026-06-23 10:08:34",
|
startTime: "2026-06-23 10:08:34",
|
||||||
endTime: "2026-06-23 11:12:25",
|
endTime: "2026-06-23 11:12:25",
|
||||||
alarms: [
|
alarms: [
|
||||||
{ id: "AL-003", type: "墙体裂缝", reportTime: "2026-06-23 10:36:41" },
|
{ id: "AL-003", type: "墙体渗漏", reportTime: "2026-06-23 10:36:41" },
|
||||||
{ id: "AL-006", type: "水渍渗漏", reportTime: "2026-06-23 10:54:18" },
|
{ id: "AL-006", type: "水渍渗漏", reportTime: "2026-06-23 10:54:18" },
|
||||||
{ id: "AL-007", type: "局部高温", reportTime: "2026-06-23 11:02:33" },
|
{ id: "AL-007", type: "局部高温", reportTime: "2026-06-23 11:02:33" },
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -71,10 +71,10 @@ export const recentAlertPhotoSources = {
|
|||||||
"桥架断裂": createAlertPhoto("crack", "#89d8ff"),
|
"桥架断裂": createAlertPhoto("crack", "#89d8ff"),
|
||||||
"隧道积水": createAlertPhoto("heat", "#ffc86c"),
|
"隧道积水": createAlertPhoto("heat", "#ffc86c"),
|
||||||
"局部高温": createAlertPhoto("heat", "#ffc86c"),
|
"局部高温": createAlertPhoto("heat", "#ffc86c"),
|
||||||
"墙体裂缝": createAlertPhoto("crack", "#89d8ff"),
|
"墙体渗漏": createAlertPhoto("crack", "#89d8ff"),
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
const fallbackAlertPhoto = recentAlertPhotoSources["墙体裂缝"];
|
const fallbackAlertPhoto = recentAlertPhotoSources["墙体渗漏"];
|
||||||
|
|
||||||
export function getRecentAlertPhoto(type: string) {
|
export function getRecentAlertPhoto(type: string) {
|
||||||
return recentAlertPhotoSources[type as keyof typeof recentAlertPhotoSources] ?? fallbackAlertPhoto;
|
return recentAlertPhotoSources[type as keyof typeof recentAlertPhotoSources] ?? fallbackAlertPhoto;
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, reactive, ref } from "vue";
|
import { computed, reactive, ref } from "vue";
|
||||||
import { useRoute, useRouter } from "vue-router";
|
import { useRoute, useRouter } from "vue-router";
|
||||||
import { useUserStore } from "@/store/modules/user.js";
|
import { useUserStore } from "@/store/modules/user";
|
||||||
import { encryptPasswordPayload } from '@/utils/passwordEncrypt.js'
|
import { encryptPasswordPayload } from '@/utils/passwordEncrypt'
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"target": "ESNext",
|
"target": "ESNext",
|
||||||
"useDefineForClassFields": true,
|
"useDefineForClassFields": true,
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"moduleResolution": "Node",
|
"moduleResolution": "bundler",
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"jsx": "preserve",
|
"jsx": "preserve",
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
@@ -12,9 +12,8 @@
|
|||||||
"lib": ["ESNext", "DOM"],
|
"lib": ["ESNext", "DOM"],
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"baseUrl": "./",
|
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["src/*"]
|
"@/*": ["./src/*"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue", "src/api/rosCar.ts"]
|
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue", "src/api/rosCar.ts"]
|
||||||
|
|||||||
@@ -1,16 +1,22 @@
|
|||||||
import vue from '@vitejs/plugin-vue'
|
import vue from '@vitejs/plugin-vue'
|
||||||
import vueJsx from '@vitejs/plugin-vue-jsx'
|
import vueJsx from '@vitejs/plugin-vue-jsx'
|
||||||
import { resolve } from 'path'
|
import { resolve } from 'path'
|
||||||
|
import Components from 'unplugin-vue-components/vite'
|
||||||
|
import { NaiveUiResolver } from 'unplugin-vue-components/resolvers'
|
||||||
|
import { defineConfig, type ConfigEnv } from 'vite'
|
||||||
|
|
||||||
// import { viteSingleFile } from 'vite-plugin-singlefile'
|
// import { viteSingleFile } from 'vite-plugin-singlefile'
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default ({ command }) => {
|
export default defineConfig(({ command }: ConfigEnv) => {
|
||||||
const isBuild = command === 'build'
|
const isBuild = command === 'build'
|
||||||
return ({
|
return ({
|
||||||
plugins: [
|
plugins: [
|
||||||
vue(),
|
vue(),
|
||||||
vueJsx(),
|
vueJsx(),
|
||||||
|
Components({
|
||||||
|
resolvers: [NaiveUiResolver()]
|
||||||
|
}),
|
||||||
],
|
],
|
||||||
define: {
|
define: {
|
||||||
__VUE_OPTIONS_API__: false // 关闭 Vue2 中的 options选项API
|
__VUE_OPTIONS_API__: false // 关闭 Vue2 中的 options选项API
|
||||||
@@ -25,9 +31,9 @@ export default ({ command }) => {
|
|||||||
host: true,
|
host: true,
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api': {
|
'/api': {
|
||||||
target: `http://192.168.3.110:16003/`,
|
target: `http://127.0.0.1:16003/`,
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
rewrite: (path) => path.replace(/^\/api/, '')
|
rewrite: (path: string) => path.replace(/^\/api/, '')
|
||||||
},
|
},
|
||||||
'/ws': {
|
'/ws': {
|
||||||
target: `ws://192.168.3.110:16003/`,
|
target: `ws://192.168.3.110:16003/`,
|
||||||
@@ -36,9 +42,18 @@ export default ({ command }) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
oxc: {
|
// Vite 8 原生方式:使用 Oxc Minifier 的 drop 选项
|
||||||
//清除全局的console.log和debug
|
build: {
|
||||||
drop: isBuild ? ['console', 'debugger'] : [],
|
rolldownOptions: {
|
||||||
}
|
output: {
|
||||||
|
minify: {
|
||||||
|
compress: {
|
||||||
|
dropConsole: isBuild,
|
||||||
|
dropDebugger: isBuild,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
})
|
||||||
Reference in New Issue
Block a user