Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0bf52a5d55 | |||
| e8d37df18e | |||
| 61e105def2 | |||
| fd753291fe | |||
| 35e98b9ab1 | |||
| 0915529af0 | |||
| 03d319ebf6 | |||
| d29075e9b4 | |||
| 10097f3003 | |||
| c352a29b4e | |||
| 7c40ae9309 |
2
.vscode/extensions.json
vendored
2
.vscode/extensions.json
vendored
@@ -1,3 +1,3 @@
|
|||||||
{
|
{
|
||||||
"recommendations": ["Vue.volar"]
|
"recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"]
|
||||||
}
|
}
|
||||||
|
|||||||
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']
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,6 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
<script type="module" src="/src/main.js"></script>
|
<script type="module" src="/src/main.ts"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
2304
package-lock.json
generated
2304
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
19
package.json
19
package.json
@@ -5,7 +5,7 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "vue-tsc && vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"format": "prettier --write ."
|
"format": "prettier --write ."
|
||||||
},
|
},
|
||||||
@@ -15,17 +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",
|
||||||
"vite": "^4.2.0"
|
"naive-ui": "^2.44.1",
|
||||||
|
"typescript": "^5.9.3",
|
||||||
|
"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 |
25
src/App.vue
25
src/App.vue
@@ -1,32 +1,40 @@
|
|||||||
<script setup>
|
<script lang="ts" setup>
|
||||||
import { computed, onBeforeUnmount, onMounted, ref } from "vue";
|
import { computed, onBeforeUnmount, onMounted, ref } from "vue";
|
||||||
|
|
||||||
const DESIGN_WIDTH = 1920;
|
const DESIGN_WIDTH = 1920;
|
||||||
const DESIGN_HEIGHT = 1080;
|
const DESIGN_HEIGHT = 1080;
|
||||||
|
|
||||||
const viewportWidth = ref(0);
|
const viewportWidth = ref(0);
|
||||||
const viewportHeight = ref(0);
|
const viewportHeight = ref(0);
|
||||||
const scale = ref(1);
|
const scale = ref(1);
|
||||||
|
|
||||||
function updateScreenAdapter() {
|
function updateScreenAdapter() {
|
||||||
viewportWidth.value = window.innerWidth;
|
viewportWidth.value = window.innerWidth;
|
||||||
viewportHeight.value = window.innerHeight;
|
viewportHeight.value = window.innerHeight;
|
||||||
|
|
||||||
const widthScale = viewportWidth.value / DESIGN_WIDTH;
|
const widthScale = viewportWidth.value / DESIGN_WIDTH;
|
||||||
const heightScale = viewportHeight.value / DESIGN_HEIGHT;
|
const heightScale = viewportHeight.value / DESIGN_HEIGHT;
|
||||||
scale.value = Math.min(widthScale, heightScale);
|
scale.value = Math.min(widthScale, heightScale);
|
||||||
}
|
}
|
||||||
|
|
||||||
const adapterStyle = computed(() => {
|
const adapterStyle = computed(() => {
|
||||||
const scaledWidth = DESIGN_WIDTH * scale.value;
|
const scaledWidth = DESIGN_WIDTH * scale.value;
|
||||||
const scaledHeight = DESIGN_HEIGHT * scale.value;
|
const scaledHeight = DESIGN_HEIGHT * scale.value;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
width: `${DESIGN_WIDTH}px`,
|
width: `${DESIGN_WIDTH}px`,
|
||||||
height: `${DESIGN_HEIGHT}px`,
|
height: `${DESIGN_HEIGHT}px`,
|
||||||
left: `${(viewportWidth.value - scaledWidth) / 2}px`,
|
left: `${(viewportWidth.value - scaledWidth) / 2}px`,
|
||||||
top: `${(viewportHeight.value - scaledHeight) / 2}px`,
|
top: `${(viewportHeight.value - scaledHeight) / 2}px`,
|
||||||
transform: `scale(${scale.value})`
|
transform: `scale(${scale.value})`,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
updateScreenAdapter();
|
updateScreenAdapter();
|
||||||
window.addEventListener("resize", updateScreenAdapter);
|
window.addEventListener("resize", updateScreenAdapter);
|
||||||
});
|
});
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
window.removeEventListener("resize", updateScreenAdapter);
|
window.removeEventListener("resize", updateScreenAdapter);
|
||||||
});
|
});
|
||||||
@@ -35,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,18 +0,0 @@
|
|||||||
import request from "@/utils/request";
|
|
||||||
function getRecentAlarmList(data) {
|
|
||||||
return request({
|
|
||||||
url: "/admin/rosInspectionRecordLineImage/warnList",
|
|
||||||
data
|
|
||||||
});
|
|
||||||
}
|
|
||||||
function getAlarmPage(data) {
|
|
||||||
return request({
|
|
||||||
url: "/admin/rosInspectionRecordLineImage/list",
|
|
||||||
method: "POST",
|
|
||||||
data
|
|
||||||
});
|
|
||||||
}
|
|
||||||
export {
|
|
||||||
getAlarmPage,
|
|
||||||
getRecentAlarmList
|
|
||||||
};
|
|
||||||
94
src/api/alarm.ts
Normal file
94
src/api/alarm.ts
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
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 {
|
||||||
|
/** 识别类型 */
|
||||||
|
recognizeType?: number | string;
|
||||||
|
/** 开始时间 */
|
||||||
|
startTime?: string;
|
||||||
|
/** 结束时间 */
|
||||||
|
endTime?: string;
|
||||||
|
/** 是否预警 */
|
||||||
|
warning?: boolean;
|
||||||
|
/** 关键字 */
|
||||||
|
keyword?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RosInspectionRecordLineImagePageQuery =
|
||||||
|
Partial<RosInspectionRecordLineImage> & {
|
||||||
|
keyword?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 巡检路线抓拍图片识别记录,对应后端 com.haizhiyustc.ros.entity.RosInspectionRecordLineImage
|
||||||
|
* recognitionStatus: 0待执行 1执行中 2执行成功 3执行失败
|
||||||
|
*/
|
||||||
|
export interface RosInspectionRecordLineImage {
|
||||||
|
/** 巡检记录id */
|
||||||
|
recordId: string;
|
||||||
|
/** 巡检设备id快照 */
|
||||||
|
carId?: string;
|
||||||
|
/** 关联的小车信息 */
|
||||||
|
car?: RosCar;
|
||||||
|
/** 抓拍动作id */
|
||||||
|
recordActionId: string;
|
||||||
|
/** 所属任务id */
|
||||||
|
taskId?: string;
|
||||||
|
/** 识别类型 */
|
||||||
|
recognizeType?: number;
|
||||||
|
/** 图片访问地址 */
|
||||||
|
imageUrl?: string;
|
||||||
|
/** 抓拍时间 */
|
||||||
|
captureTime?: string;
|
||||||
|
/** 抓拍时的小车位姿x */
|
||||||
|
poseX?: number;
|
||||||
|
/** 抓拍时的小车位姿y */
|
||||||
|
poseY?: number;
|
||||||
|
/** 抓拍时的小车朝向 */
|
||||||
|
poseYaw?: number;
|
||||||
|
/** 识别状态:0待执行 1执行中 2执行成功 3执行失败 */
|
||||||
|
recognitionStatus?: number;
|
||||||
|
/** AI识别提示词 */
|
||||||
|
prompt?: string;
|
||||||
|
/** AI识别结果 */
|
||||||
|
recognitionResult?: string;
|
||||||
|
/** 是否预警 */
|
||||||
|
warning?: boolean;
|
||||||
|
/** 预警值 */
|
||||||
|
warningValue?: string;
|
||||||
|
/** 失败原因 */
|
||||||
|
failReason?: string;
|
||||||
|
/** 开始识别时间 */
|
||||||
|
startTime?: string;
|
||||||
|
/** 结束识别时间 */
|
||||||
|
endTime?: string;
|
||||||
|
/** 创建时间 */
|
||||||
|
createTime?: string;
|
||||||
|
/** 更新时间 */
|
||||||
|
updateTime?: string;
|
||||||
|
/** 主键id */
|
||||||
|
id?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询近期告警列表
|
||||||
|
// 返回告警列表,对应后端 warnList 接口
|
||||||
|
// 返回: ApiResult<List<RosInspectionRecordLineImage>>
|
||||||
|
export function getRecentAlarmList(data: RosInspectionRecordLineImageQueryDTO) {
|
||||||
|
return request<ApiResult<RosInspectionRecordLineImage[]>>({
|
||||||
|
url: "/admin/rosInspectionRecordLineImage/warnList",
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页查询告警列表
|
||||||
|
export function getAlarmPage(data: PageParam<RosInspectionRecordLineImagePageQuery>) {
|
||||||
|
return request<ApiResult<PageResult<RosInspectionRecordLineImage>>>({
|
||||||
|
url: "/admin/rosInspectionRecordLineImage/list",
|
||||||
|
method: "POST",
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
import request from "@/utils/request";
|
|
||||||
function getRosCarList(data) {
|
|
||||||
return request({
|
|
||||||
url: "/admin/rosCar/list",
|
|
||||||
data
|
|
||||||
});
|
|
||||||
}
|
|
||||||
function saveRosInspectionTask(data) {
|
|
||||||
return request({
|
|
||||||
url: "/admin/rosInspectionTask/save",
|
|
||||||
data
|
|
||||||
});
|
|
||||||
}
|
|
||||||
function getRosInspectionTaskDetail(cardId) {
|
|
||||||
return request({
|
|
||||||
url: "/admin/rosInspectionTask/detail",
|
|
||||||
params: { cardId }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
function getCurrentRosInspection(carId) {
|
|
||||||
return request({
|
|
||||||
url: "/admin/rosCar/currentInspection",
|
|
||||||
params: { carId }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
function dispatchRosInspectionTask(taskId) {
|
|
||||||
return request({
|
|
||||||
url: "/admin/rosInspectionTask/dispatch",
|
|
||||||
params: { taskId }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
function stopDispatchRosInspectionTask(taskId) {
|
|
||||||
return request({
|
|
||||||
url: "/admin/rosInspectionTask/stopDispatch",
|
|
||||||
params: { taskId }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
function uploadFile(file) {
|
|
||||||
const data = new FormData();
|
|
||||||
data.append("file", file);
|
|
||||||
return request({
|
|
||||||
url: "/admin/file/upload",
|
|
||||||
data
|
|
||||||
});
|
|
||||||
}
|
|
||||||
function updateRosCarMapUrls(data) {
|
|
||||||
return request({
|
|
||||||
url: "/admin/rosCar/updateMapUrls",
|
|
||||||
data
|
|
||||||
});
|
|
||||||
}
|
|
||||||
export {
|
|
||||||
dispatchRosInspectionTask,
|
|
||||||
getCurrentRosInspection,
|
|
||||||
getRosCarList,
|
|
||||||
getRosInspectionTaskDetail,
|
|
||||||
saveRosInspectionTask,
|
|
||||||
stopDispatchRosInspectionTask,
|
|
||||||
updateRosCarMapUrls,
|
|
||||||
uploadFile
|
|
||||||
};
|
|
||||||
295
src/api/rosCar.ts
Normal file
295
src/api/rosCar.ts
Normal file
@@ -0,0 +1,295 @@
|
|||||||
|
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: PageParam<RosCarQuery>) {
|
||||||
|
return request<ApiResult<PageResult<RosCar>>>({
|
||||||
|
url: "/admin/rosCar/list",
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存巡检任务模板
|
||||||
|
export function saveRosInspectionTask(data: unknown) {
|
||||||
|
return request<ApiResult<null>>({
|
||||||
|
url: "/admin/rosInspectionTask/save",
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 巡检任务模板详情
|
||||||
|
export function getRosInspectionTaskDetail(cardId: string) {
|
||||||
|
return request<ApiResult<RosInspectionTaskDetailDTO>>({
|
||||||
|
url: "/admin/rosInspectionTask/detail",
|
||||||
|
params: { cardId },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询小车当前巡检状态
|
||||||
|
export function getCurrentRosInspection(carId: string) {
|
||||||
|
return request<ApiResult<RosInspectionRecord>>({
|
||||||
|
url: "/admin/rosCar/currentInspection",
|
||||||
|
params: { carId },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 下发巡检任务
|
||||||
|
export function dispatchRosInspectionTask(taskId: string) {
|
||||||
|
return request<ApiResult<RosDispatchResultDTO>>({
|
||||||
|
url: "/admin/rosInspectionTask/dispatch",
|
||||||
|
params: { taskId },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 停止已下发的巡检任务
|
||||||
|
export function stopDispatchRosInspectionTask(taskId: string) {
|
||||||
|
return request<ApiResult<RosDispatchResultDTO>>({
|
||||||
|
url: "/admin/rosInspectionTask/stopDispatch",
|
||||||
|
params: { taskId },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 上传文件并返回后端保存后的访问地址
|
||||||
|
export function uploadFile(file: File) {
|
||||||
|
const data = new FormData();
|
||||||
|
|
||||||
|
data.append("file", file);
|
||||||
|
|
||||||
|
return request<ApiResult<string>>({
|
||||||
|
url: "/admin/file/upload",
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新小车地图文件地址
|
||||||
|
export function updateRosCarMapUrls(data: {
|
||||||
|
id: string;
|
||||||
|
mapPgmUrl?: string;
|
||||||
|
mapYamlUrl?: string;
|
||||||
|
}) {
|
||||||
|
return request<ApiResult<null>>({
|
||||||
|
url: "/admin/rosCar/updateMapUrls",
|
||||||
|
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
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
@font-face {
|
|
||||||
/*给字体命名*/
|
|
||||||
font-family: 'YouSheBiaoTiHei';
|
|
||||||
/*引入字体文件*/
|
|
||||||
src: url('./YouSheBiaoTiHei.ttf');
|
|
||||||
font-weight: normal;
|
|
||||||
font-style: normal;
|
|
||||||
}
|
|
||||||
Binary file not shown.
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,45 +1,42 @@
|
|||||||
<script setup>
|
<script lang="ts" setup>
|
||||||
import { nextTick, onBeforeUnmount, onMounted, ref, watch } from "vue";
|
import { onBeforeUnmount, onMounted, ref, watch } from "vue";
|
||||||
import * as echarts from "echarts";
|
import { init } from "echarts/core";
|
||||||
let chart = null;
|
import type { EChartsOption } from "echarts";
|
||||||
let resizeObserver = null;
|
|
||||||
const chartRef = ref();
|
let chart: ReturnType<typeof init> | null = null;
|
||||||
|
|
||||||
|
const chartRef = ref<HTMLElement | undefined>();
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
option: {
|
option: {
|
||||||
type: Object,
|
type: Object as () => EChartsOption | {},
|
||||||
required: true
|
required: true,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
function applyOption(option) {
|
|
||||||
|
function applyOption(option?: EChartsOption | Record<string, never>) {
|
||||||
if (!chart || !option) {
|
if (!chart || !option) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
chart.clear();
|
chart.clear();
|
||||||
chart.setOption(option);
|
chart.setOption(option);
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
chart = echarts.init(chartRef.value, "t-theme");
|
chart = init(chartRef.value, "t-theme");
|
||||||
applyOption(props.option);
|
applyOption(props.option);
|
||||||
resizeObserver = new ResizeObserver(() => {
|
|
||||||
chart?.resize();
|
|
||||||
});
|
|
||||||
resizeObserver.observe(chartRef.value);
|
|
||||||
nextTick(() => {
|
|
||||||
chart?.resize();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.option,
|
() => props.option,
|
||||||
(value) => {
|
(value) => {
|
||||||
applyOption(value);
|
applyOption(value);
|
||||||
},
|
},
|
||||||
{ deep: true }
|
{ deep: true },
|
||||||
);
|
);
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
if (resizeObserver) {
|
|
||||||
resizeObserver.disconnect();
|
|
||||||
resizeObserver = null;
|
|
||||||
}
|
|
||||||
if (chart) {
|
if (chart) {
|
||||||
chart.dispose();
|
chart.dispose();
|
||||||
chart = null;
|
chart = null;
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ const color = [
|
|||||||
colorStops: [
|
colorStops: [
|
||||||
{ offset: 0, color: "#94c7fd" },
|
{ offset: 0, color: "#94c7fd" },
|
||||||
{ offset: 0.5, color: "#3b99fb" },
|
{ offset: 0.5, color: "#3b99fb" },
|
||||||
{ offset: 1, color: "#004ca0" }
|
{ offset: 1, color: "#004ca0" },
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "linear",
|
type: "linear",
|
||||||
@@ -20,8 +20,8 @@ const color = [
|
|||||||
colorStops: [
|
colorStops: [
|
||||||
{ offset: 0, color: "#b070c0" },
|
{ offset: 0, color: "#b070c0" },
|
||||||
{ offset: 0.5, color: "#b070c0" },
|
{ offset: 0.5, color: "#b070c0" },
|
||||||
{ offset: 1, color: "#5d235d" }
|
{ offset: 1, color: "#5d235d" },
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "linear",
|
type: "linear",
|
||||||
@@ -32,8 +32,8 @@ const color = [
|
|||||||
colorStops: [
|
colorStops: [
|
||||||
{ offset: 0, color: "#7eedd6" },
|
{ offset: 0, color: "#7eedd6" },
|
||||||
{ offset: 0.5, color: "#3be8bb" },
|
{ offset: 0.5, color: "#3be8bb" },
|
||||||
{ offset: 1, color: "#00b38a" }
|
{ offset: 1, color: "#00b38a" },
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "linear",
|
type: "linear",
|
||||||
@@ -44,8 +44,8 @@ const color = [
|
|||||||
colorStops: [
|
colorStops: [
|
||||||
{ offset: 0, color: "#fff1f0" },
|
{ offset: 0, color: "#fff1f0" },
|
||||||
{ offset: 0.5, color: "#ff9494" },
|
{ offset: 0.5, color: "#ff9494" },
|
||||||
{ offset: 1, color: "#ff4d4f" }
|
{ offset: 1, color: "#ff4d4f" },
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "linear",
|
type: "linear",
|
||||||
@@ -56,8 +56,8 @@ const color = [
|
|||||||
colorStops: [
|
colorStops: [
|
||||||
{ offset: 0, color: "#fffbe6" },
|
{ offset: 0, color: "#fffbe6" },
|
||||||
{ offset: 0.5, color: "#ffe58f" },
|
{ offset: 0.5, color: "#ffe58f" },
|
||||||
{ offset: 1, color: "#ffbb96" }
|
{ offset: 1, color: "#ffbb96" },
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "linear",
|
type: "linear",
|
||||||
@@ -68,8 +68,8 @@ const color = [
|
|||||||
colorStops: [
|
colorStops: [
|
||||||
{ offset: 0, color: "#ff3333" },
|
{ offset: 0, color: "#ff3333" },
|
||||||
{ offset: 0.5, color: "#ff0000" },
|
{ offset: 0.5, color: "#ff0000" },
|
||||||
{ offset: 1, color: "#cc0000" }
|
{ offset: 1, color: "#cc0000" },
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "linear",
|
type: "linear",
|
||||||
@@ -80,24 +80,25 @@ const color = [
|
|||||||
colorStops: [
|
colorStops: [
|
||||||
{ offset: 0, color: "#ffe6f0" },
|
{ offset: 0, color: "#ffe6f0" },
|
||||||
{ offset: 0.5, color: "#ffb3d9" },
|
{ offset: 0.5, color: "#ffb3d9" },
|
||||||
{ offset: 1, color: "#ff85c0" }
|
{ offset: 1, color: "#ff85c0" },
|
||||||
]
|
],
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
color,
|
color,
|
||||||
textStyle: {
|
textStyle: {
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: "#fff"
|
color: "#fff",
|
||||||
},
|
},
|
||||||
legend: {
|
legend: {
|
||||||
textStyle: {
|
textStyle: {
|
||||||
color: "#fff"
|
color: "#fff",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
tooltip: {
|
tooltip: {
|
||||||
textStyle: {
|
textStyle: {
|
||||||
align: "left"
|
align: "left",
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
@@ -1,30 +1,35 @@
|
|||||||
|
import { Map } from "ol";
|
||||||
import VectorSource from "ol/source/Vector";
|
import VectorSource from "ol/source/Vector";
|
||||||
import { GeoJSON } from "ol/format";
|
import { GeoJSON } from "ol/format";
|
||||||
import VectorLayer from "ol/layer/Vector";
|
import VectorLayer from "ol/layer/Vector";
|
||||||
import { Fill, Stroke, Style } from "ol/style";
|
import { Fill, Stroke, Style } from "ol/style";
|
||||||
export default function (mapInstance) {
|
import { FeatureLike } from "ol/Feature";
|
||||||
|
import { MultiPolygon } from "ol/geom";
|
||||||
|
|
||||||
|
export default function (mapInstance: Map) {
|
||||||
const vectorSource = new VectorSource({
|
const vectorSource = new VectorSource({
|
||||||
url: "https://geo.datav.aliyun.com/areas_v3/bound/330000.json",
|
url: "https://geo.datav.aliyun.com/areas_v3/bound/330000.json",
|
||||||
format: new GeoJSON()
|
format: new GeoJSON(),
|
||||||
});
|
});
|
||||||
|
|
||||||
for (let i = 1; i <= 3; i++) {
|
for (let i = 1; i <= 3; i++) {
|
||||||
const layer = new VectorLayer({
|
const layer = new VectorLayer({
|
||||||
zIndex: -1,
|
zIndex: -1,
|
||||||
source: vectorSource,
|
source: vectorSource,
|
||||||
style: (feature) => {
|
style: (feature: FeatureLike) => {
|
||||||
const geometry = feature.getGeometry().clone();
|
const geometry = (feature.getGeometry() as MultiPolygon).clone();
|
||||||
geometry.translate(0, -0.04 * i);
|
geometry.translate(0, -0.04 * i);
|
||||||
return new Style({
|
return new Style({
|
||||||
fill: new Fill({
|
fill: new Fill({
|
||||||
color: "rgba(176, 166, 132,0.6)"
|
color: "rgba(176, 166, 132,0.6)",
|
||||||
}),
|
}),
|
||||||
stroke: new Stroke({
|
stroke: new Stroke({
|
||||||
color: "rgba(176, 166, 132,0.6)",
|
color: "rgba(176, 166, 132,0.6)",
|
||||||
width: 1
|
width: 1,
|
||||||
}),
|
}),
|
||||||
geometry
|
geometry,
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
mapInstance.addLayer(layer);
|
mapInstance.addLayer(layer);
|
||||||
}
|
}
|
||||||
@@ -7,28 +7,33 @@ import { Fill, Stroke, Style, Text } from "ol/style";
|
|||||||
import { getBottomLeft, getCenter, getHeight, getWidth } from "ol/extent";
|
import { getBottomLeft, getCenter, getHeight, getWidth } from "ol/extent";
|
||||||
import materialJpg from "@/assets/map-material.jpg";
|
import materialJpg from "@/assets/map-material.jpg";
|
||||||
import { toContext } from "ol/render";
|
import { toContext } from "ol/render";
|
||||||
|
import { MultiPolygon } from "ol/geom";
|
||||||
import { defaults } from "ol/interaction";
|
import { defaults } from "ol/interaction";
|
||||||
function useMap(target) {
|
|
||||||
|
export default function useMap(target: string) {
|
||||||
const mapInstance = new Map({
|
const mapInstance = new Map({
|
||||||
view: new View({
|
view: new View({
|
||||||
projection: "EPSG:4326",
|
projection: "EPSG:4326",
|
||||||
zoom: 12,
|
zoom: 12,
|
||||||
center: [0, 0]
|
center: [0, 0],
|
||||||
}),
|
}),
|
||||||
controls: [],
|
controls: [],
|
||||||
interactions: defaults({
|
interactions: defaults({
|
||||||
dragPan: false,
|
dragPan: false,
|
||||||
mouseWheelZoom: false
|
mouseWheelZoom: false,
|
||||||
})
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
const vectorSource = new VectorSource({
|
const vectorSource = new VectorSource({
|
||||||
url: "https://geo.datav.aliyun.com/areas_v3/bound/330000_full.json",
|
url: "https://geo.datav.aliyun.com/areas_v3/bound/330000_full.json",
|
||||||
format: new GeoJSON()
|
format: new GeoJSON(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const customStyle = new Style({
|
const customStyle = new Style({
|
||||||
renderer: function(pixelCoordinates, state) {
|
renderer: function (pixelCoordinates, state) {
|
||||||
const context = state.context;
|
const context = state.context;
|
||||||
const multiPolygon = state.geometry.clone();
|
const multiPolygon = state.geometry.clone() as MultiPolygon;
|
||||||
|
// @ts-ignore
|
||||||
multiPolygon.setCoordinates(pixelCoordinates);
|
multiPolygon.setCoordinates(pixelCoordinates);
|
||||||
const extent = multiPolygon.getExtent();
|
const extent = multiPolygon.getExtent();
|
||||||
const width = getWidth(extent);
|
const width = getWidth(extent);
|
||||||
@@ -39,14 +44,14 @@ function useMap(target) {
|
|||||||
}
|
}
|
||||||
context.save();
|
context.save();
|
||||||
const renderContext = toContext(context, {
|
const renderContext = toContext(context, {
|
||||||
pixelRatio: 1
|
pixelRatio: 1,
|
||||||
});
|
});
|
||||||
renderContext.setFillStrokeStyle(
|
renderContext.setFillStrokeStyle(
|
||||||
new Fill(),
|
new Fill(),
|
||||||
new Stroke({
|
new Stroke({
|
||||||
color: "rgba(255,255,255,0.8)",
|
color: "rgba(255,255,255,0.8)",
|
||||||
width: 3
|
width: 3,
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
renderContext.drawMultiPolygon(multiPolygon);
|
renderContext.drawMultiPolygon(multiPolygon);
|
||||||
context.clip();
|
context.clip();
|
||||||
@@ -55,8 +60,9 @@ function useMap(target) {
|
|||||||
const bottom = bottomLeft[1];
|
const bottom = bottomLeft[1];
|
||||||
context.drawImage(flag, left, bottom, width, height);
|
context.drawImage(flag, left, bottom, width, height);
|
||||||
context.restore();
|
context.restore();
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const layer = new VectorLayer({
|
const layer = new VectorLayer({
|
||||||
source: vectorSource,
|
source: vectorSource,
|
||||||
style: (feature) => {
|
style: (feature) => {
|
||||||
@@ -66,35 +72,40 @@ function useMap(target) {
|
|||||||
text: new Text({
|
text: new Text({
|
||||||
text: feature.get("name"),
|
text: feature.get("name"),
|
||||||
fill: new Fill({
|
fill: new Fill({
|
||||||
color: "#fff"
|
color: "#fff",
|
||||||
}),
|
}),
|
||||||
scale: 1.5
|
scale: 1.5,
|
||||||
|
}),
|
||||||
|
zIndex: 1,
|
||||||
}),
|
}),
|
||||||
zIndex: 1
|
|
||||||
})
|
|
||||||
];
|
];
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
vectorSource.on("addfeature", function(event) {
|
|
||||||
|
vectorSource.on("addfeature", function (event) {
|
||||||
const feature = event.feature;
|
const feature = event.feature;
|
||||||
const img = new Image();
|
const img = new Image();
|
||||||
img.src = materialJpg;
|
img.src = materialJpg;
|
||||||
img.onload = function() {
|
img.onload = function () {
|
||||||
feature?.set("material", img);
|
feature?.set("material", img);
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
vectorSource.on("featuresloadend", (event) => {
|
vectorSource.on("featuresloadend", (event) => {
|
||||||
const view = mapInstance.getView();
|
const view = mapInstance.getView();
|
||||||
const extent = event.target.getExtent();
|
const extent = event.target.getExtent();
|
||||||
view.fit(extent, { padding: [0, 30, 0, 0] });
|
view.fit(extent, { padding: [0, 30, 0, 0] });
|
||||||
|
|
||||||
view.setCenter(getCenter(extent));
|
view.setCenter(getCenter(extent));
|
||||||
});
|
});
|
||||||
|
|
||||||
mapInstance.addLayer(layer);
|
mapInstance.addLayer(layer);
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
mapInstance.setTarget(target);
|
mapInstance.setTarget(target);
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
mapInstance
|
mapInstance,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
export default useMap;
|
|
||||||
@@ -2,19 +2,23 @@ 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 naive, { createDiscreteApi } from "naive-ui";
|
|
||||||
import { createPinia } from "pinia";
|
import { createPinia } from "pinia";
|
||||||
import router from "./router";
|
import router from "./router";
|
||||||
import '../src/assets/font/YouSheBiaoTiHei.css';
|
import "./router/routerGuard";
|
||||||
echarts.registerTheme("t-theme", echartsConfig);
|
|
||||||
const { message, dialog } = createDiscreteApi(["message", "dialog"]);
|
// CanvasRenderer 注册到全局,所有图表都需要
|
||||||
window.$message = message;
|
import { use } from "echarts/core";
|
||||||
window.$modal = dialog;
|
use([CanvasRenderer]);
|
||||||
|
|
||||||
|
registerTheme("t-theme", echartsConfig);
|
||||||
|
|
||||||
const app = createApp(App);
|
const app = createApp(App);
|
||||||
|
|
||||||
app.use(createPinia());
|
app.use(createPinia());
|
||||||
app.use(router);
|
app.use(router);
|
||||||
app.use(naive);
|
|
||||||
app.component("TChart", Chart);
|
app.component("TChart", Chart);
|
||||||
|
|
||||||
app.mount("#app");
|
app.mount("#app");
|
||||||
@@ -1,13 +1,16 @@
|
|||||||
import { getToken } from "@/utils/auth";
|
import { getToken } from "@/utils/auth";
|
||||||
import {
|
import {
|
||||||
createRouter,
|
createRouter,
|
||||||
createWebHashHistory
|
createWebHashHistory,
|
||||||
|
type RouteRecordRaw,
|
||||||
} from "vue-router";
|
} from "vue-router";
|
||||||
|
|
||||||
const dashboardDefaultPath = "/dashboard/integrated-center";
|
const dashboardDefaultPath = "/dashboard/integrated-center";
|
||||||
const routes = [
|
|
||||||
|
const routes: RouteRecordRaw[] = [
|
||||||
{
|
{
|
||||||
path: "/",
|
path: "/",
|
||||||
redirect: () => getToken() ? dashboardDefaultPath : "/login"
|
redirect: () => (getToken() ? dashboardDefaultPath : "/login"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/dashboard",
|
path: "/dashboard",
|
||||||
@@ -17,67 +20,61 @@ const routes = [
|
|||||||
{
|
{
|
||||||
path: "integrated-center",
|
path: "integrated-center",
|
||||||
name: "IntegratedCenter",
|
name: "IntegratedCenter",
|
||||||
component: () => import("@/views/dashboard1/pages/IntegratedCenterPage.vue"),
|
component: () =>
|
||||||
meta: { title: "监管中心", tabId: 1 }
|
import("@/views/dashboard1/pages/IntegratedCenterPage.vue"),
|
||||||
|
meta: { title: "监管中心", tabId: 1, },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "equipment-management",
|
path: "equipment-management",
|
||||||
name: "EquipmentManagement",
|
name: "EquipmentManagement",
|
||||||
component: () => import("@/views/dashboard1/pages/EquipmentManagementPage.vue"),
|
component: () =>
|
||||||
meta: { title: "设备管理", tabId: 2 }
|
import("@/views/dashboard1/pages/EquipmentManagementPage.vue"),
|
||||||
|
meta: { title: "设备管理", tabId: 2, },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "video-center",
|
path: "video-center",
|
||||||
name: "VideoCenter",
|
name: "VideoCenter",
|
||||||
component: () => import("@/views/dashboard1/pages/VideoCenterPage.vue"),
|
component: () => import("@/views/dashboard1/pages/VideoCenterPage.vue"),
|
||||||
meta: { title: "视频中心", tabId: 3 }
|
meta: { title: "视频中心", tabId: 3, },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "alarm-management",
|
path: "alarm-management",
|
||||||
name: "AlarmManagement",
|
name: "AlarmManagement",
|
||||||
component: () => import("@/views/dashboard1/pages/AlarmManagementPage.vue"),
|
component: () =>
|
||||||
meta: { title: "告警管理", tabId: 4 }
|
import("@/views/dashboard1/pages/AlarmManagementPage.vue"),
|
||||||
|
meta: { title: "告警管理", tabId: 4, },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "patrol-plan",
|
path: "patrol-plan",
|
||||||
name: "PatrolPlan",
|
name: "PatrolPlan",
|
||||||
component: () => import("@/views/dashboard1/pages/PatrolPlanPage.vue"),
|
component: () => import("@/views/dashboard1/pages/PatrolPlanPage.vue"),
|
||||||
meta: { title: "巡检记录", tabId: 5 }
|
meta: { title: "巡检记录", tabId: 5, },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "data-report",
|
path: "data-report",
|
||||||
name: "DataReport",
|
name: "DataReport",
|
||||||
component: () => import("@/views/dashboard1/pages/DataReportPage.vue"),
|
component: () => import("@/views/dashboard1/pages/DataReportPage.vue"),
|
||||||
meta: { title: "统计分析", tabId: 6 }
|
meta: { title: "统计分析", tabId: 6, },
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/login",
|
path: "/login",
|
||||||
name: "Login",
|
name: "Login",
|
||||||
component: () => import("@/views/login/index.vue"),
|
component: () => import("@/views/login/index.vue"),
|
||||||
meta: { title: "登录" }
|
meta: { title: "登录" },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/:pathMatch(.*)*",
|
path: "/:pathMatch(.*)*",
|
||||||
redirect: dashboardDefaultPath
|
redirect: dashboardDefaultPath,
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHashHistory(),
|
history: createWebHashHistory(),
|
||||||
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: !useDark(),
|
isDark: !window.matchMedia('(prefers-color-scheme: dark)').matches,
|
||||||
}
|
}),
|
||||||
},
|
|
||||||
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;
|
||||||
|
}
|
||||||
20
src/types/js-modules.d.ts
vendored
Normal file
20
src/types/js-modules.d.ts
vendored
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
interface Window {
|
||||||
|
$message: {
|
||||||
|
error(message: string, options?: Record<string, unknown>): void;
|
||||||
|
success(message: string, options?: Record<string, unknown>): void;
|
||||||
|
warning(message: string, options?: Record<string, unknown>): void;
|
||||||
|
info(message: string, options?: Record<string, unknown>): void;
|
||||||
|
};
|
||||||
|
$modal: {
|
||||||
|
create(options: Record<string, unknown>): unknown;
|
||||||
|
warning(options: Record<string, unknown>): unknown;
|
||||||
|
};
|
||||||
|
$loadingBar: {
|
||||||
|
start(): void;
|
||||||
|
finish(): void;
|
||||||
|
error(): void;
|
||||||
|
[key: string]: unknown;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
declare var $loadingBar: Window["$loadingBar"];
|
||||||
@@ -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,4 +1,10 @@
|
|||||||
function generateNumbers(min, max, count) {
|
function generateNumbers(min: number, max: number, count: number): number[];
|
||||||
|
function generateNumbers(min: number, max: number): number;
|
||||||
|
function generateNumbers(
|
||||||
|
min: number,
|
||||||
|
max: number,
|
||||||
|
count?: number,
|
||||||
|
): number | number[] {
|
||||||
if (!count) {
|
if (!count) {
|
||||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||||
}
|
}
|
||||||
@@ -9,6 +15,5 @@ function generateNumbers(min, max, count) {
|
|||||||
}
|
}
|
||||||
return numbers;
|
return numbers;
|
||||||
}
|
}
|
||||||
export {
|
|
||||||
generateNumbers
|
export { generateNumbers };
|
||||||
};
|
|
||||||
@@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
import { defineComponent } from "vue";
|
|
||||||
import style from "./index.module.less";
|
|
||||||
export default defineComponent({
|
|
||||||
name: "switchPageBtn",
|
|
||||||
props: {
|
|
||||||
direction: {
|
|
||||||
type: String,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
text: {
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
active: Boolean
|
|
||||||
},
|
|
||||||
setup(props) {
|
|
||||||
return () => <div
|
|
||||||
class={[
|
|
||||||
style.btn,
|
|
||||||
props.active ? style.active : style.normal,
|
|
||||||
style[props.direction]
|
|
||||||
]}
|
|
||||||
><div class={[style.text]}>{props.text}</div></div>;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
29
src/views/dashboard1/components/btn/index.tsx
Normal file
29
src/views/dashboard1/components/btn/index.tsx
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { defineComponent } from "vue";
|
||||||
|
import style from "./index.module.less";
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: "switchPageBtn",
|
||||||
|
props: {
|
||||||
|
direction: {
|
||||||
|
type: String as () => "left" | "right",
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
active: Boolean,
|
||||||
|
},
|
||||||
|
setup(props) {
|
||||||
|
return () => (
|
||||||
|
<div
|
||||||
|
class={[
|
||||||
|
style.btn,
|
||||||
|
props.active ? style.active : style.normal,
|
||||||
|
style[props.direction],
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<div class={[style.text]}>{props.text}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -1,44 +1,51 @@
|
|||||||
import { onBeforeUnmount, ref } from "vue";
|
import { onBeforeUnmount, ref } from "vue";
|
||||||
|
import { EChartsOption } from "echarts";
|
||||||
import { generateNumbers } from "@/utils";
|
import { generateNumbers } from "@/utils";
|
||||||
|
|
||||||
export default function () {
|
export default function () {
|
||||||
const option = ref({});
|
const option = ref<EChartsOption>({});
|
||||||
|
|
||||||
function refresh() {
|
function refresh() {
|
||||||
option.value = {
|
option.value = {
|
||||||
xAxis: {
|
xAxis: {
|
||||||
type: "category",
|
type: "category",
|
||||||
data: ["周一", "周二", "周三", "周四", "周五", "周六", "周日"]
|
data: ["周一", "周二", "周三", "周四", "周五", "周六", "周日"],
|
||||||
},
|
},
|
||||||
yAxis: {
|
yAxis: {
|
||||||
type: "value"
|
type: "value",
|
||||||
},
|
},
|
||||||
grid: {
|
grid: {
|
||||||
left: "12%"
|
left: "12%",
|
||||||
},
|
},
|
||||||
series: [
|
series: [
|
||||||
{
|
{
|
||||||
data: generateNumbers(1, 300, 7),
|
data: generateNumbers(1, 300, 7),
|
||||||
type: "line"
|
type: "line",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
data: generateNumbers(1, 300, 7),
|
data: generateNumbers(1, 300, 7),
|
||||||
type: "line"
|
type: "line",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
data: generateNumbers(1, 300, 7),
|
data: generateNumbers(1, 300, 7),
|
||||||
type: "line"
|
type: "line",
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
refresh();
|
refresh();
|
||||||
|
|
||||||
const timer = setInterval(() => {
|
const timer = setInterval(() => {
|
||||||
refresh();
|
refresh();
|
||||||
}, 3e3);
|
}, 3000);
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
clearInterval(timer);
|
clearInterval(timer);
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
option,
|
option,
|
||||||
refresh
|
refresh,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -1,15 +1,18 @@
|
|||||||
import { onBeforeUnmount, ref } from "vue";
|
import { onBeforeUnmount, ref } from "vue";
|
||||||
|
import { EChartsOption } from "echarts";
|
||||||
import { generateNumbers } from "@/utils";
|
import { generateNumbers } from "@/utils";
|
||||||
|
|
||||||
export default function () {
|
export default function () {
|
||||||
const option = ref({});
|
const option = ref<EChartsOption>({});
|
||||||
|
|
||||||
function refresh() {
|
function refresh() {
|
||||||
option.value = {
|
option.value = {
|
||||||
tooltip: {
|
tooltip: {
|
||||||
trigger: "item"
|
trigger: "item",
|
||||||
},
|
},
|
||||||
legend: {
|
legend: {
|
||||||
top: "5%",
|
top: "5%",
|
||||||
left: "center"
|
left: "center",
|
||||||
},
|
},
|
||||||
series: [
|
series: [
|
||||||
{
|
{
|
||||||
@@ -18,42 +21,46 @@ export default function () {
|
|||||||
radius: ["40%", "70%"],
|
radius: ["40%", "70%"],
|
||||||
avoidLabelOverlap: false,
|
avoidLabelOverlap: false,
|
||||||
itemStyle: {
|
itemStyle: {
|
||||||
borderColor: "#fff"
|
borderColor: "#fff",
|
||||||
},
|
},
|
||||||
label: {
|
label: {
|
||||||
show: false,
|
show: false,
|
||||||
position: "center"
|
position: "center",
|
||||||
},
|
},
|
||||||
emphasis: {
|
emphasis: {
|
||||||
label: {
|
label: {
|
||||||
show: true,
|
show: true,
|
||||||
fontSize: 30,
|
fontSize: 30,
|
||||||
fontWeight: "bold"
|
fontWeight: "bold",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
labelLine: {
|
labelLine: {
|
||||||
show: false
|
show: false,
|
||||||
},
|
},
|
||||||
data: [
|
data: [
|
||||||
{ value: generateNumbers(10, 100), name: "产品一" },
|
{ value: generateNumbers(10, 100), name: "产品一" },
|
||||||
{ value: generateNumbers(10, 100), name: "产品二" },
|
{ value: generateNumbers(10, 100), name: "产品二" },
|
||||||
{ value: generateNumbers(10, 100), name: "产品三" },
|
{ value: generateNumbers(10, 100), name: "产品三" },
|
||||||
{ value: generateNumbers(10, 100), name: "产品四" },
|
{ value: generateNumbers(10, 100), name: "产品四" },
|
||||||
{ value: generateNumbers(10, 100), name: "产品五" }
|
{ value: generateNumbers(10, 100), name: "产品五" },
|
||||||
]
|
],
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
refresh();
|
refresh();
|
||||||
|
|
||||||
const timer = setInterval(() => {
|
const timer = setInterval(() => {
|
||||||
refresh();
|
refresh();
|
||||||
}, 3e3);
|
}, 3000);
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
clearInterval(timer);
|
clearInterval(timer);
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
option,
|
option,
|
||||||
refresh
|
refresh,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -1,12 +1,15 @@
|
|||||||
import { onBeforeUnmount, ref } from "vue";
|
import { onBeforeUnmount, ref } from "vue";
|
||||||
|
import { EChartsOption } from "echarts";
|
||||||
import { generateNumbers } from "@/utils";
|
import { generateNumbers } from "@/utils";
|
||||||
|
|
||||||
export default function () {
|
export default function () {
|
||||||
const option = ref({});
|
const option = ref<EChartsOption>({});
|
||||||
|
|
||||||
function refresh() {
|
function refresh() {
|
||||||
option.value = {
|
option.value = {
|
||||||
tooltip: {
|
tooltip: {
|
||||||
trigger: "item",
|
trigger: "item",
|
||||||
formatter: "{a} <br/>{b} : {c}%"
|
formatter: "{a} <br/>{b} : {c}%",
|
||||||
},
|
},
|
||||||
series: [
|
series: [
|
||||||
{
|
{
|
||||||
@@ -15,53 +18,57 @@ export default function () {
|
|||||||
left: "center",
|
left: "center",
|
||||||
width: "100%",
|
width: "100%",
|
||||||
min: 0,
|
min: 0,
|
||||||
max: 1e3,
|
max: 1000,
|
||||||
minSize: "0%",
|
minSize: "0%",
|
||||||
maxSize: "100%",
|
maxSize: "100%",
|
||||||
sort: "descending",
|
sort: "descending",
|
||||||
gap: 2,
|
gap: 2,
|
||||||
label: {
|
label: {
|
||||||
show: true,
|
show: true,
|
||||||
position: "inside"
|
position: "inside",
|
||||||
},
|
},
|
||||||
labelLine: {
|
labelLine: {
|
||||||
length: 10,
|
length: 10,
|
||||||
lineStyle: {
|
lineStyle: {
|
||||||
width: 1,
|
width: 1,
|
||||||
type: "solid"
|
type: "solid",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
itemStyle: {
|
itemStyle: {
|
||||||
borderColor: "#fff",
|
borderColor: "#fff",
|
||||||
borderWidth: 1
|
borderWidth: 1,
|
||||||
},
|
},
|
||||||
emphasis: {
|
emphasis: {
|
||||||
label: {
|
label: {
|
||||||
fontSize: 20
|
fontSize: 20,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
data: [
|
data: [
|
||||||
{ value: generateNumbers(500, 1e3), name: "访问量" },
|
{ value: generateNumbers(500, 1000), name: "访问量" },
|
||||||
{ value: generateNumbers(500, 1e3), name: "注册量" },
|
{ value: generateNumbers(500, 1000), name: "注册量" },
|
||||||
{ value: generateNumbers(500, 1e3), name: "浏览量" },
|
{ value: generateNumbers(500, 1000), name: "浏览量" },
|
||||||
{ value: generateNumbers(200, 500), name: "加入购物车" },
|
{ value: generateNumbers(200, 500), name: "加入购物车" },
|
||||||
{ value: generateNumbers(100, 800), name: "购买" },
|
{ value: generateNumbers(100, 800), name: "购买" },
|
||||||
{ value: generateNumbers(1, 20), name: "退货" },
|
{ value: generateNumbers(1, 20), name: "退货" },
|
||||||
{ value: generateNumbers(1, 20), name: "取消订单" }
|
{ value: generateNumbers(1, 20), name: "取消订单" },
|
||||||
]
|
],
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
refresh();
|
refresh();
|
||||||
|
|
||||||
const timer = setInterval(() => {
|
const timer = setInterval(() => {
|
||||||
refresh();
|
refresh();
|
||||||
}, 3e3);
|
}, 3000);
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
clearInterval(timer);
|
clearInterval(timer);
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
option,
|
option,
|
||||||
refresh
|
refresh,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
import { onBeforeUnmount, ref } from "vue";
|
import { onBeforeUnmount, ref } from "vue";
|
||||||
|
import { EChartsOption } from "echarts";
|
||||||
import { generateNumbers } from "@/utils";
|
import { generateNumbers } from "@/utils";
|
||||||
|
|
||||||
export default function () {
|
export default function () {
|
||||||
const option = ref({});
|
const option = ref<EChartsOption>({});
|
||||||
|
|
||||||
function refresh() {
|
function refresh() {
|
||||||
option.value = {
|
option.value = {
|
||||||
radar: {
|
radar: {
|
||||||
@@ -11,9 +14,9 @@ export default function () {
|
|||||||
{ name: "品质", max: 200 },
|
{ name: "品质", max: 200 },
|
||||||
{ name: "客户支持", max: 200 },
|
{ name: "客户支持", max: 200 },
|
||||||
{ name: "可持续性", max: 200 },
|
{ name: "可持续性", max: 200 },
|
||||||
{ name: "效果", max: 200 }
|
{ name: "效果", max: 200 },
|
||||||
],
|
],
|
||||||
radius: 100
|
radius: 100,
|
||||||
},
|
},
|
||||||
series: [
|
series: [
|
||||||
{
|
{
|
||||||
@@ -21,22 +24,26 @@ export default function () {
|
|||||||
data: [
|
data: [
|
||||||
{
|
{
|
||||||
value: generateNumbers(1, 200, 6),
|
value: generateNumbers(1, 200, 6),
|
||||||
areaStyle: {}
|
areaStyle: {},
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
refresh();
|
refresh();
|
||||||
|
|
||||||
const timer = setInterval(() => {
|
const timer = setInterval(() => {
|
||||||
refresh();
|
refresh();
|
||||||
}, 3e3);
|
}, 3000);
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
clearInterval(timer);
|
clearInterval(timer);
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
option,
|
option,
|
||||||
refresh
|
refresh,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -1,40 +1,43 @@
|
|||||||
import { onBeforeUnmount, ref } from "vue";
|
import { onBeforeUnmount, ref } from "vue";
|
||||||
|
import { EChartsOption } from "echarts";
|
||||||
import { generateNumbers } from "@/utils";
|
import { generateNumbers } from "@/utils";
|
||||||
|
|
||||||
export default function () {
|
export default function () {
|
||||||
const option = ref({});
|
const option = ref<EChartsOption>({});
|
||||||
|
|
||||||
function refresh() {
|
function refresh() {
|
||||||
option.value = {
|
option.value = {
|
||||||
tooltip: {
|
tooltip: {
|
||||||
trigger: "axis",
|
trigger: "axis",
|
||||||
axisPointer: {
|
axisPointer: {
|
||||||
type: "shadow"
|
type: "shadow",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
legend: {
|
legend: {
|
||||||
top: "5%",
|
top: "5%",
|
||||||
itemStyle: {
|
itemStyle: {
|
||||||
borderColor: "#fff",
|
borderColor: "#fff",
|
||||||
borderWidth: 2
|
borderWidth: 2,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
grid: {
|
grid: {
|
||||||
left: "10%",
|
left: "10%",
|
||||||
bottom: "8%",
|
bottom: "8%",
|
||||||
containLabel: true
|
containLabel: true,
|
||||||
},
|
},
|
||||||
xAxis: [
|
xAxis: [
|
||||||
{
|
{
|
||||||
type: "value"
|
type: "value",
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
yAxis: [
|
yAxis: [
|
||||||
{
|
{
|
||||||
type: "category",
|
type: "category",
|
||||||
axisTick: {
|
axisTick: {
|
||||||
show: false
|
show: false,
|
||||||
|
},
|
||||||
|
data: ["一月", "二月", "三月", "四月", "五月", "六月", "七月"],
|
||||||
},
|
},
|
||||||
data: ["一月", "二月", "三月", "四月", "五月", "六月", "七月"]
|
|
||||||
}
|
|
||||||
],
|
],
|
||||||
series: [
|
series: [
|
||||||
{
|
{
|
||||||
@@ -42,49 +45,53 @@ export default function () {
|
|||||||
type: "bar",
|
type: "bar",
|
||||||
label: {
|
label: {
|
||||||
show: true,
|
show: true,
|
||||||
position: "inside"
|
position: "inside",
|
||||||
},
|
},
|
||||||
emphasis: {
|
emphasis: {
|
||||||
focus: "series"
|
focus: "series",
|
||||||
},
|
},
|
||||||
data: generateNumbers(1, 200, 7)
|
data: generateNumbers(1, 200, 7),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "购买",
|
name: "购买",
|
||||||
type: "bar",
|
type: "bar",
|
||||||
stack: "Total",
|
stack: "Total",
|
||||||
label: {
|
label: {
|
||||||
show: true
|
show: true,
|
||||||
},
|
},
|
||||||
emphasis: {
|
emphasis: {
|
||||||
focus: "series"
|
focus: "series",
|
||||||
},
|
},
|
||||||
data: generateNumbers(1, 300, 7)
|
data: generateNumbers(1, 300, 7),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "退货",
|
name: "退货",
|
||||||
type: "bar",
|
type: "bar",
|
||||||
stack: "Total",
|
stack: "Total",
|
||||||
label: {
|
label: {
|
||||||
show: true
|
show: true,
|
||||||
},
|
},
|
||||||
emphasis: {
|
emphasis: {
|
||||||
focus: "series"
|
focus: "series",
|
||||||
},
|
},
|
||||||
data: generateNumbers(-50, -10, 7)
|
data: generateNumbers(-50, -10, 7),
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
refresh();
|
refresh();
|
||||||
|
|
||||||
const timer = setInterval(() => {
|
const timer = setInterval(() => {
|
||||||
refresh();
|
refresh();
|
||||||
}, 3e3);
|
}, 3000);
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
clearInterval(timer);
|
clearInterval(timer);
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
option,
|
option,
|
||||||
refresh
|
refresh,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
import { onBeforeUnmount, ref } from "vue";
|
import { onBeforeUnmount, ref } from "vue";
|
||||||
|
import { EChartsOption } from "echarts";
|
||||||
import { generateNumbers } from "@/utils";
|
import { generateNumbers } from "@/utils";
|
||||||
|
|
||||||
export default function () {
|
export default function () {
|
||||||
const option = ref({});
|
const option = ref<EChartsOption>({});
|
||||||
|
|
||||||
function refresh() {
|
function refresh() {
|
||||||
option.value = {
|
option.value = {
|
||||||
xAxis: {},
|
xAxis: {},
|
||||||
@@ -16,8 +19,8 @@ export default function () {
|
|||||||
generateNumbers(1, 10, 2),
|
generateNumbers(1, 10, 2),
|
||||||
generateNumbers(1, 10, 2),
|
generateNumbers(1, 10, 2),
|
||||||
[8.07, 6.95],
|
[8.07, 6.95],
|
||||||
[13, 7.58],
|
[13.0, 7.58],
|
||||||
[14, 7.66],
|
[14.0, 7.66],
|
||||||
[13.4, 6.81],
|
[13.4, 6.81],
|
||||||
generateNumbers(1, 10, 2),
|
generateNumbers(1, 10, 2),
|
||||||
generateNumbers(1, 10, 2),
|
generateNumbers(1, 10, 2),
|
||||||
@@ -31,22 +34,26 @@ export default function () {
|
|||||||
generateNumbers(1, 10, 2),
|
generateNumbers(1, 10, 2),
|
||||||
generateNumbers(1, 10, 2),
|
generateNumbers(1, 10, 2),
|
||||||
generateNumbers(1, 10, 2),
|
generateNumbers(1, 10, 2),
|
||||||
[5.02, 5.68]
|
[5.02, 5.68],
|
||||||
|
],
|
||||||
|
type: "scatter",
|
||||||
|
},
|
||||||
],
|
],
|
||||||
type: "scatter"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
refresh();
|
refresh();
|
||||||
|
|
||||||
const timer = setInterval(() => {
|
const timer = setInterval(() => {
|
||||||
refresh();
|
refresh();
|
||||||
}, 3e3);
|
}, 3000);
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
clearInterval(timer);
|
clearInterval(timer);
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
option,
|
option,
|
||||||
refresh
|
refresh,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -2,15 +2,6 @@
|
|||||||
width: 1920px;
|
width: 1920px;
|
||||||
height: 1080px;
|
height: 1080px;
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
&:after {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
z-index: 1;
|
|
||||||
pointer-events: none;
|
|
||||||
background-image: url("./assets/头部动画.webp"), url("./assets/头部动画.png"),
|
background-image: url("./assets/头部动画.webp"), url("./assets/头部动画.png"),
|
||||||
url("./assets/整体边框_静态图片.png"), url("./assets/底部动画.webp"),
|
url("./assets/整体边框_静态图片.png"), url("./assets/底部动画.webp"),
|
||||||
url("./assets/底部动画.png"), url("./assets/头部动画_左侧点.webp"),
|
url("./assets/底部动画.png"), url("./assets/头部动画_左侧点.webp"),
|
||||||
@@ -36,33 +27,23 @@
|
|||||||
auto,
|
auto,
|
||||||
auto,
|
auto,
|
||||||
auto;
|
auto;
|
||||||
}
|
|
||||||
|
|
||||||
&:before {
|
&:before {
|
||||||
content: "管廊巡检机器人综合管理平台";
|
content: "管廊巡检机器人综合管理平台";
|
||||||
color: #FFFFFF;
|
color: #fff;
|
||||||
background: linear-gradient(0deg, #02B3FE 0%, #F9FDFF 100%);
|
letter-spacing: 12px;
|
||||||
background-clip: text;
|
font-size: 36px;
|
||||||
-webkit-background-clip: text;
|
line-height: 70px;
|
||||||
-webkit-text-fill-color: transparent;
|
width: 100%;
|
||||||
font-family: YouSheBiaoTiHei, system-ui;
|
font-weight: bold;
|
||||||
font-size: 42px;
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
z-index: 2;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.background {
|
.background {
|
||||||
position: absolute;
|
position: relative;
|
||||||
inset: 0;
|
z-index: -1;
|
||||||
z-index: 0;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
@@ -70,7 +51,6 @@
|
|||||||
|
|
||||||
.main {
|
.main {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 2;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
top: 0;
|
top: 0;
|
||||||
@@ -276,27 +256,6 @@
|
|||||||
overflow: auto;
|
overflow: auto;
|
||||||
border: 1px solid rgba(77, 188, 248, 0.12);
|
border: 1px solid rgba(77, 188, 248, 0.12);
|
||||||
background: rgba(5, 22, 46, 0.46);
|
background: rgba(5, 22, 46, 0.46);
|
||||||
scrollbar-width: thin;
|
|
||||||
scrollbar-color: rgba(92, 215, 255, 0.48) rgba(5, 22, 46, 0.72);
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-shell::-webkit-scrollbar {
|
|
||||||
width: 8px;
|
|
||||||
height: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-shell::-webkit-scrollbar-track {
|
|
||||||
background: rgba(5, 22, 46, 0.72);
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-shell::-webkit-scrollbar-thumb {
|
|
||||||
border-radius: 999px;
|
|
||||||
background: linear-gradient(180deg, rgba(107, 230, 255, 0.78), rgba(27, 121, 202, 0.72));
|
|
||||||
box-shadow: inset 0 0 6px rgba(178, 245, 255, 0.28);
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-shell::-webkit-scrollbar-thumb:hover {
|
|
||||||
background: linear-gradient(180deg, rgba(146, 242, 255, 0.95), rgba(43, 154, 231, 0.88));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.device-table {
|
.device-table {
|
||||||
@@ -308,8 +267,7 @@
|
|||||||
.device-table th,
|
.device-table th,
|
||||||
.device-table td {
|
.device-table td {
|
||||||
padding: 14px 16px;
|
padding: 14px 16px;
|
||||||
text-align: center;
|
text-align: left;
|
||||||
vertical-align: middle;
|
|
||||||
border-bottom: 1px solid rgba(77, 188, 248, 0.08);
|
border-bottom: 1px solid rgba(77, 188, 248, 0.08);
|
||||||
color: #dff7ff;
|
color: #dff7ff;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
@@ -423,7 +381,6 @@
|
|||||||
.table-actions {
|
.table-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -486,31 +443,6 @@
|
|||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pagination-jumper {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
color: rgba(174, 226, 247, 0.78);
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pagination-select {
|
|
||||||
height: 34px;
|
|
||||||
min-width: 96px;
|
|
||||||
padding: 0 28px 0 12px;
|
|
||||||
border: 1px solid rgba(87, 198, 255, 0.18);
|
|
||||||
background: rgba(8, 31, 62, 0.72);
|
|
||||||
color: #dff8ff;
|
|
||||||
font-size: 12px;
|
|
||||||
outline: none;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pagination-select:disabled {
|
|
||||||
opacity: 0.4;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.crud-dialog-mask {
|
.crud-dialog-mask {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
@@ -1712,17 +1644,6 @@
|
|||||||
drop-shadow(0 0 28px rgba(20, 94, 160, 0.18));
|
drop-shadow(0 0 28px rgba(20, 94, 160, 0.18));
|
||||||
}
|
}
|
||||||
|
|
||||||
.meter-grid-fill {
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.meter-grid-line {
|
|
||||||
fill: none;
|
|
||||||
stroke: rgba(139, 236, 255, 0.22);
|
|
||||||
stroke-width: 1;
|
|
||||||
vector-effect: non-scaling-stroke;
|
|
||||||
}
|
|
||||||
|
|
||||||
.map-state-tip {
|
.map-state-tip {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 3;
|
z-index: 3;
|
||||||
|
|||||||
@@ -1,24 +1,34 @@
|
|||||||
<script setup>
|
<script lang="ts" setup>
|
||||||
import { computed } from "vue";
|
import { computed } from "vue";
|
||||||
import { useRoute, useRouter } from "vue-router";
|
import { useRoute, useRouter } from "vue-router";
|
||||||
import SwitchPageBtn from "./components/btn/index.jsx";
|
import SwitchPageBtn from "./components/btn";
|
||||||
import backgroundVideo from "./assets/循环背景动画.mp4";
|
import backgroundVideo from "./assets/循环背景动画.mp4";
|
||||||
|
|
||||||
|
interface DashboardTab {
|
||||||
|
id: number;
|
||||||
|
text: string;
|
||||||
|
path: string;
|
||||||
|
}
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const tabs = {
|
|
||||||
|
const tabs: Record<"left" | "right", DashboardTab[]> = {
|
||||||
left: [
|
left: [
|
||||||
{ id: 1, text: "监管中心", path: "/dashboard/integrated-center" },
|
{ id: 1, text: "监管中心", path: "/dashboard/integrated-center" },
|
||||||
{ id: 2, text: "设备管理", path: "/dashboard/equipment-management" },
|
{ id: 2, text: "设备管理", path: "/dashboard/equipment-management" },
|
||||||
{ id: 3, text: "视频中心", path: "/dashboard/video-center" }
|
{ id: 3, text: "视频中心", path: "/dashboard/video-center" },
|
||||||
],
|
],
|
||||||
right: [
|
right: [
|
||||||
{ id: 4, text: "告警管理", path: "/dashboard/alarm-management" },
|
{ id: 4, text: "告警管理", path: "/dashboard/alarm-management" },
|
||||||
{ id: 5, text: "巡检记录", path: "/dashboard/patrol-plan" },
|
{ id: 5, text: "巡检记录", path: "/dashboard/patrol-plan" },
|
||||||
{ id: 6, text: "统计分析", path: "/dashboard/data-report" }
|
{ id: 6, text: "统计分析", path: "/dashboard/data-report" },
|
||||||
]
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
const activeTabId = computed(() => Number(route.meta.tabId) || 1);
|
const activeTabId = computed(() => Number(route.meta.tabId) || 1);
|
||||||
function switchPage(tab) {
|
|
||||||
|
function switchPage(tab: DashboardTab) {
|
||||||
if (route.path !== tab.path) {
|
if (route.path !== tab.path) {
|
||||||
router.push(tab.path);
|
router.push(tab.path);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,65 +1,104 @@
|
|||||||
<script setup>
|
<script lang="ts" setup>
|
||||||
|
import { NImage } from "naive-ui";
|
||||||
import { computed, onMounted, ref } from "vue";
|
import { computed, onMounted, ref } from "vue";
|
||||||
import {
|
import {
|
||||||
getAlarmPage
|
getAlarmPage,
|
||||||
|
type RosInspectionRecordLineImage,
|
||||||
} from "@/api/alarm";
|
} from "@/api/alarm";
|
||||||
import { BASE_URL } from "@/utils/request";
|
import { BASE_URL } from "@/utils/request";
|
||||||
import { getRecentAlertPhoto } from "../shared/alertPhotos";
|
import { getRecentAlertPhoto } from "../shared/alertPhotos";
|
||||||
const recognizeTypeMap = {
|
|
||||||
|
interface AlarmRecord {
|
||||||
|
id: string;
|
||||||
|
type: string;
|
||||||
|
robot: string;
|
||||||
|
photo: string;
|
||||||
|
status: RecognitionStatusText;
|
||||||
|
time: string;
|
||||||
|
position: string;
|
||||||
|
result: string;
|
||||||
|
warning: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
type RecognitionStatusText = "待执行" | "执行中" | "执行成功" | "执行失败" | "未知状态";
|
||||||
|
type WarningFilterValue = "" | "true" | "false";
|
||||||
|
|
||||||
|
interface AlarmPageResult {
|
||||||
|
total?: number;
|
||||||
|
data?: RosInspectionRecordLineImage[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AlarmPageResponse {
|
||||||
|
data?: AlarmPageResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
const recognizeTypeMap: Record<number, string> = {
|
||||||
100: "局部高温",
|
100: "局部高温",
|
||||||
1: "电缆表面破损",
|
1: "电缆表面破损",
|
||||||
2: "桥架断裂",
|
2: "桥架断裂",
|
||||||
3: "隧道积水",
|
3: "隧道积水",
|
||||||
4: "墙体裂缝"
|
4: "墙体渗漏",
|
||||||
};
|
};
|
||||||
const recognitionStatusMap = {
|
|
||||||
|
const recognitionStatusMap: Record<number, RecognitionStatusText> = {
|
||||||
0: "待执行",
|
0: "待执行",
|
||||||
1: "执行中",
|
1: "执行中",
|
||||||
2: "执行成功",
|
2: "执行成功",
|
||||||
3: "执行失败"
|
3: "执行失败",
|
||||||
};
|
};
|
||||||
|
|
||||||
const alarmNo = ref("");
|
const alarmNo = ref("");
|
||||||
const recognizeType = ref("");
|
const recognizeType = ref("");
|
||||||
const warningFilter = ref("true");
|
const warningFilter = ref<WarningFilterValue>("true");
|
||||||
const startTime = ref("");
|
const startTime = ref("");
|
||||||
const endTime = ref("");
|
const endTime = ref("");
|
||||||
const alarms = ref([]);
|
const alarms = ref<AlarmRecord[]>([]);
|
||||||
const totalAlarms = ref(0);
|
const totalAlarms = ref(0);
|
||||||
const listLoading = ref(false);
|
const listLoading = ref(false);
|
||||||
const listError = ref("");
|
const listError = ref("");
|
||||||
const pageSize = 10;
|
const pageSize = 10;
|
||||||
const currentPage = ref(1);
|
const currentPage = ref(1);
|
||||||
const totalPages = computed(
|
|
||||||
() => Math.max(1, Math.ceil(totalAlarms.value / pageSize))
|
const totalPages = computed(() =>
|
||||||
|
Math.max(1, Math.ceil(totalAlarms.value / pageSize)),
|
||||||
);
|
);
|
||||||
|
|
||||||
const pagedAlarms = computed(() => alarms.value);
|
const pagedAlarms = computed(() => alarms.value);
|
||||||
|
|
||||||
const visiblePages = computed(() => {
|
const visiblePages = computed(() => {
|
||||||
const pages = [];
|
const pages: number[] = [];
|
||||||
const start = Math.max(1, currentPage.value - 2);
|
const start = Math.max(1, currentPage.value - 2);
|
||||||
const end = Math.min(totalPages.value, start + 4);
|
const end = Math.min(totalPages.value, start + 4);
|
||||||
|
|
||||||
for (let page = start; page <= end; page += 1) {
|
for (let page = start; page <= end; page += 1) {
|
||||||
pages.push(page);
|
pages.push(page);
|
||||||
}
|
}
|
||||||
|
|
||||||
return pages;
|
return pages;
|
||||||
});
|
});
|
||||||
function getAlarmStatusClass(status) {
|
|
||||||
const classMap = {
|
function getAlarmStatusClass(status: AlarmRecord["status"]) {
|
||||||
|
const classMap: Record<AlarmRecord["status"], string> = {
|
||||||
待执行: "alarm-status-pending",
|
待执行: "alarm-status-pending",
|
||||||
执行中: "alarm-status-handling",
|
执行中: "alarm-status-handling",
|
||||||
执行成功: "alarm-status-finished",
|
执行成功: "alarm-status-finished",
|
||||||
执行失败: "alarm-status-false",
|
执行失败: "alarm-status-false",
|
||||||
未知状态: "alarm-status-false"
|
未知状态: "alarm-status-false",
|
||||||
};
|
};
|
||||||
|
|
||||||
return classMap[status];
|
return classMap[status];
|
||||||
}
|
}
|
||||||
function setPage(page) {
|
|
||||||
|
function setPage(page: number) {
|
||||||
currentPage.value = Math.min(Math.max(page, 1), totalPages.value);
|
currentPage.value = Math.min(Math.max(page, 1), totalPages.value);
|
||||||
fetchAlarmList();
|
fetchAlarmList();
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSearch() {
|
function handleSearch() {
|
||||||
currentPage.value = 1;
|
currentPage.value = 1;
|
||||||
fetchAlarmList();
|
fetchAlarmList();
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleClear() {
|
function handleClear() {
|
||||||
alarmNo.value = "";
|
alarmNo.value = "";
|
||||||
recognizeType.value = "";
|
recognizeType.value = "";
|
||||||
@@ -68,23 +107,28 @@ function handleClear() {
|
|||||||
endTime.value = "";
|
endTime.value = "";
|
||||||
handleSearch();
|
handleSearch();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 请求告警分页列表并更新表格数据
|
||||||
async function fetchAlarmList() {
|
async function fetchAlarmList() {
|
||||||
listLoading.value = true;
|
listLoading.value = true;
|
||||||
listError.value = "";
|
listError.value = "";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
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),
|
||||||
startTime: normalizeDateTime(startTime.value),
|
startTime: normalizeDateTime(startTime.value),
|
||||||
endTime: normalizeDateTime(endTime.value),
|
endTime: normalizeDateTime(endTime.value),
|
||||||
id: alarmNo.value.trim() || void 0
|
id: alarmNo.value.trim() || undefined,
|
||||||
}
|
},
|
||||||
});
|
})) as AlarmPageResponse;
|
||||||
const result = response.data;
|
const result = response.data;
|
||||||
const rows = result?.data ?? [];
|
const rows = result?.data ?? [];
|
||||||
|
|
||||||
alarms.value = rows.map((record, index) => normalizeAlarmRecord(record, index));
|
alarms.value = rows.map((record, index) => normalizeAlarmRecord(record, index));
|
||||||
totalAlarms.value = result?.total ?? rows.length;
|
totalAlarms.value = result?.total ?? rows.length;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -96,8 +140,11 @@ async function fetchAlarmList() {
|
|||||||
listLoading.value = false;
|
listLoading.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function normalizeAlarmRecord(record, index) {
|
|
||||||
|
// 将后端告警记录转换为页面表格展示结构
|
||||||
|
function normalizeAlarmRecord(record: RosInspectionRecordLineImage, index: number): AlarmRecord {
|
||||||
const type = getRecognizeTypeText(record.recognizeType);
|
const type = getRecognizeTypeText(record.recognizeType);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: record.id || record.recordActionId || record.recordId || `AL-${index + 1}`,
|
id: record.id || record.recordActionId || record.recordId || `AL-${index + 1}`,
|
||||||
type,
|
type,
|
||||||
@@ -107,45 +154,64 @@ function normalizeAlarmRecord(record, index) {
|
|||||||
time: record.captureTime || record.createTime || "--",
|
time: record.captureTime || record.createTime || "--",
|
||||||
position: formatPosition(record),
|
position: formatPosition(record),
|
||||||
result: record.warningValue || record.recognitionResult || record.failReason || "--",
|
result: record.warningValue || record.recognitionResult || record.failReason || "--",
|
||||||
warning: Boolean(record.warning)
|
warning: Boolean(record.warning),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
function getCarName(record) {
|
|
||||||
|
function getCarName(record: RosInspectionRecordLineImage) {
|
||||||
return record.car?.name || record.car?.deviceNo || record.car?.deviceKey || record.carId || "未知设备";
|
return record.car?.name || record.car?.deviceNo || record.car?.deviceKey || record.carId || "未知设备";
|
||||||
}
|
}
|
||||||
function getRecognizeTypeText(type) {
|
|
||||||
|
function getRecognizeTypeText(type?: number) {
|
||||||
return type ? recognizeTypeMap[type] ?? "未知告警" : "未知告警";
|
return type ? recognizeTypeMap[type] ?? "未知告警" : "未知告警";
|
||||||
}
|
}
|
||||||
function getRecognitionStatusText(status) {
|
|
||||||
|
function getRecognitionStatusText(status?: number) {
|
||||||
return typeof status === "number" ? recognitionStatusMap[status] ?? "未知状态" : "未知状态";
|
return typeof status === "number" ? recognitionStatusMap[status] ?? "未知状态" : "未知状态";
|
||||||
}
|
}
|
||||||
function buildFileAccessUrl(fileUrl) {
|
|
||||||
if (!fileUrl || /^(https?:)?\/\//i.test(fileUrl) || fileUrl.startsWith("blob:") || fileUrl.startsWith("data:")) {
|
// 相对文件地址通过接口代理访问,避免开发环境直接请求后端静态路径失败
|
||||||
|
function buildFileAccessUrl(fileUrl: string) {
|
||||||
|
if (
|
||||||
|
!fileUrl ||
|
||||||
|
/^(https?:)?\/\//i.test(fileUrl) ||
|
||||||
|
fileUrl.startsWith("blob:") ||
|
||||||
|
fileUrl.startsWith("data:")
|
||||||
|
) {
|
||||||
return fileUrl;
|
return fileUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fileUrl === BASE_URL || fileUrl.startsWith(`${BASE_URL}/`)) {
|
if (fileUrl === BASE_URL || fileUrl.startsWith(`${BASE_URL}/`)) {
|
||||||
return fileUrl;
|
return fileUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
return `${BASE_URL}${fileUrl.startsWith("/") ? fileUrl : `/${fileUrl}`}`;
|
return `${BASE_URL}${fileUrl.startsWith("/") ? fileUrl : `/${fileUrl}`}`;
|
||||||
}
|
}
|
||||||
function normalizeDateTime(value) {
|
|
||||||
return value ? value.replace("T", " ") : void 0;
|
function normalizeDateTime(value: string) {
|
||||||
|
return value ? value.replace("T", " ") : undefined;
|
||||||
}
|
}
|
||||||
function normalizeRecognizeType(value) {
|
|
||||||
return value ? Number(value) : void 0;
|
function normalizeRecognizeType(value: string) {
|
||||||
|
return value ? Number(value) : undefined;
|
||||||
}
|
}
|
||||||
function normalizeWarningFilter(value) {
|
|
||||||
|
function normalizeWarningFilter(value: WarningFilterValue) {
|
||||||
if (!value) {
|
if (!value) {
|
||||||
return void 0;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
return value === "true";
|
return value === "true";
|
||||||
}
|
}
|
||||||
function formatPosition(record) {
|
|
||||||
|
function formatPosition(record: RosInspectionRecordLineImage) {
|
||||||
if (typeof record.poseX !== "number" || typeof record.poseY !== "number") {
|
if (typeof record.poseX !== "number" || typeof record.poseY !== "number") {
|
||||||
return "--";
|
return "--";
|
||||||
}
|
}
|
||||||
|
|
||||||
return `X:${record.poseX.toFixed(2)} Y:${record.poseY.toFixed(2)}`;
|
return `X:${record.poseX.toFixed(2)} Y:${record.poseY.toFixed(2)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
fetchAlarmList();
|
fetchAlarmList();
|
||||||
});
|
});
|
||||||
@@ -163,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>
|
||||||
@@ -189,6 +255,7 @@ onMounted(() => {
|
|||||||
/>
|
/>
|
||||||
<button class="page-action-btn primary" type="button" @click="handleSearch">搜索</button>
|
<button class="page-action-btn primary" type="button" @click="handleSearch">搜索</button>
|
||||||
<button class="page-action-btn ghost" type="button" @click="handleClear">清空</button>
|
<button class="page-action-btn ghost" type="button" @click="handleClear">清空</button>
|
||||||
|
<button class="page-action-btn ghost" type="button" @click="fetchAlarmList">刷新</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="toolbar-right">共 {{ totalAlarms }} 条告警记录</div>
|
<div class="toolbar-right">共 {{ totalAlarms }} 条告警记录</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -239,11 +306,7 @@ onMounted(() => {
|
|||||||
<td>{{ alarm.time }}</td>
|
<td>{{ alarm.time }}</td>
|
||||||
<td>{{ alarm.position }}</td>
|
<td>{{ alarm.position }}</td>
|
||||||
<td>{{ alarm.result }}</td>
|
<td>{{ alarm.result }}</td>
|
||||||
<td>
|
<td>{{ alarm.warning ? "是" : "否" }}</td>
|
||||||
<NTag :type="alarm.warning ? 'error' : 'success'" size="small">
|
|
||||||
{{ alarm.warning ? "是" : "否" }}
|
|
||||||
</NTag>
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@@ -276,19 +339,6 @@ onMounted(() => {
|
|||||||
>
|
>
|
||||||
下一页
|
下一页
|
||||||
</button>
|
</button>
|
||||||
<label class="pagination-jumper">
|
|
||||||
<span>跳至</span>
|
|
||||||
<select
|
|
||||||
class="pagination-select"
|
|
||||||
:disabled="listLoading"
|
|
||||||
:value="currentPage"
|
|
||||||
@change="setPage(Number($event.target.value))"
|
|
||||||
>
|
|
||||||
<option v-for="page in totalPages" :key="page" :value="page">
|
|
||||||
第 {{ page }} 页
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,29 +1,64 @@
|
|||||||
<script setup>
|
<script lang="ts" setup>
|
||||||
import { computed, ref } from "vue";
|
import { computed, ref } from "vue";
|
||||||
|
import type { EChartsOption } from "echarts";
|
||||||
import Chart from "@/components/chart/index.vue";
|
import Chart from "@/components/chart/index.vue";
|
||||||
const distributionRange = ref("近一周");
|
|
||||||
const reportData = [
|
// 按需注册 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 ProjectName = "西江隧道" | "青云隧道" | "南山隧道";
|
||||||
|
type DistributionRange = "近一周" | "近一个月" | "近三个月";
|
||||||
|
|
||||||
|
interface AlarmReportRecord {
|
||||||
|
id: string;
|
||||||
|
type: string;
|
||||||
|
robot: string;
|
||||||
|
project: ProjectName;
|
||||||
|
status: AlarmStatus;
|
||||||
|
time: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TrendRecord {
|
||||||
|
date: string;
|
||||||
|
project: ProjectName;
|
||||||
|
total: number;
|
||||||
|
closed: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const distributionRange = ref<DistributionRange>("近一周");
|
||||||
|
|
||||||
|
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" },
|
||||||
];
|
];
|
||||||
const trendData = [
|
|
||||||
|
const trendData: TrendRecord[] = [
|
||||||
{ date: "06-09", project: "西江隧道", total: 1, closed: 1 },
|
{ date: "06-09", project: "西江隧道", total: 1, closed: 1 },
|
||||||
{ date: "06-09", project: "青云隧道", total: 1, closed: 0 },
|
{ date: "06-09", project: "青云隧道", total: 1, closed: 0 },
|
||||||
{ date: "06-09", project: "南山隧道", total: 1, closed: 1 },
|
{ date: "06-09", project: "南山隧道", total: 1, closed: 1 },
|
||||||
@@ -68,48 +103,53 @@ const trendData = [
|
|||||||
{ date: "06-22", project: "南山隧道", total: 4, closed: 3 },
|
{ date: "06-22", project: "南山隧道", total: 4, closed: 3 },
|
||||||
{ date: "06-23", project: "西江隧道", total: 4, closed: 3 },
|
{ date: "06-23", project: "西江隧道", total: 4, closed: 3 },
|
||||||
{ date: "06-23", project: "青云隧道", total: 3, closed: 2 },
|
{ date: "06-23", project: "青云隧道", total: 3, closed: 2 },
|
||||||
{ date: "06-23", project: "南山隧道", total: 4, closed: 3 }
|
{ date: "06-23", project: "南山隧道", total: 4, closed: 3 },
|
||||||
];
|
];
|
||||||
|
|
||||||
const trendSeries = computed(() => {
|
const trendSeries = computed(() => {
|
||||||
const grouped = trendData.reduce(
|
const grouped = trendData.reduce<Record<string, { total: number; closed: number }>>(
|
||||||
(accumulator, item) => {
|
(accumulator, item) => {
|
||||||
if (!accumulator[item.date]) {
|
if (!accumulator[item.date]) {
|
||||||
accumulator[item.date] = { total: 0, closed: 0 };
|
accumulator[item.date] = { total: 0, closed: 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
accumulator[item.date].total += item.total;
|
accumulator[item.date].total += item.total;
|
||||||
accumulator[item.date].closed += item.closed;
|
accumulator[item.date].closed += item.closed;
|
||||||
return accumulator;
|
return accumulator;
|
||||||
},
|
},
|
||||||
{}
|
{},
|
||||||
);
|
);
|
||||||
|
|
||||||
return Object.entries(grouped).map(([date, value]) => ({
|
return Object.entries(grouped).map(([date, value]) => ({
|
||||||
date,
|
date,
|
||||||
total: value.total,
|
total: value.total,
|
||||||
closed: value.closed
|
closed: value.closed,
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
const trendOption = computed(() => {
|
|
||||||
|
const trendOption = computed<EChartsOption>(() => {
|
||||||
const dates = trendSeries.value.map((item) => item.date);
|
const dates = trendSeries.value.map((item) => item.date);
|
||||||
const totalValues = trendSeries.value.map((item) => item.total);
|
const totalValues = trendSeries.value.map((item) => item.total);
|
||||||
const closedValues = trendSeries.value.map((item) => item.closed);
|
const closedValues = trendSeries.value.map((item) => item.closed);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
grid: {
|
grid: {
|
||||||
left: 24,
|
left: 24,
|
||||||
right: 16,
|
right: 16,
|
||||||
top: 24,
|
top: 24,
|
||||||
bottom: 28,
|
bottom: 28,
|
||||||
containLabel: true
|
containLabel: true,
|
||||||
},
|
},
|
||||||
tooltip: {
|
tooltip: {
|
||||||
trigger: "axis",
|
trigger: "axis",
|
||||||
backgroundColor: "rgba(6, 20, 42, 0.92)",
|
backgroundColor: "rgba(6, 20, 42, 0.92)",
|
||||||
borderColor: "rgba(96, 208, 255, 0.24)",
|
borderColor: "rgba(96, 208, 255, 0.24)",
|
||||||
textStyle: {
|
textStyle: {
|
||||||
color: "#eefcff"
|
color: "#eefcff",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
legend: {
|
legend: {
|
||||||
show: false
|
show: false,
|
||||||
},
|
},
|
||||||
xAxis: {
|
xAxis: {
|
||||||
type: "category",
|
type: "category",
|
||||||
@@ -117,35 +157,35 @@ const trendOption = computed(() => {
|
|||||||
data: dates,
|
data: dates,
|
||||||
axisLine: {
|
axisLine: {
|
||||||
lineStyle: {
|
lineStyle: {
|
||||||
color: "rgba(96, 188, 242, 0.2)"
|
color: "rgba(96, 188, 242, 0.2)",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
axisLabel: {
|
axisLabel: {
|
||||||
color: "rgba(181, 230, 249, 0.74)",
|
color: "rgba(181, 230, 249, 0.74)",
|
||||||
fontSize: 11
|
fontSize: 11,
|
||||||
},
|
},
|
||||||
axisTick: {
|
axisTick: {
|
||||||
show: false
|
show: false,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
yAxis: {
|
yAxis: {
|
||||||
type: "value",
|
type: "value",
|
||||||
splitNumber: 5,
|
splitNumber: 5,
|
||||||
axisLine: {
|
axisLine: {
|
||||||
show: false
|
show: false,
|
||||||
},
|
},
|
||||||
axisTick: {
|
axisTick: {
|
||||||
show: false
|
show: false,
|
||||||
},
|
},
|
||||||
axisLabel: {
|
axisLabel: {
|
||||||
color: "rgba(181, 230, 249, 0.62)",
|
color: "rgba(181, 230, 249, 0.62)",
|
||||||
fontSize: 11
|
fontSize: 11,
|
||||||
},
|
},
|
||||||
splitLine: {
|
splitLine: {
|
||||||
lineStyle: {
|
lineStyle: {
|
||||||
color: "rgba(97, 189, 242, 0.12)"
|
color: "rgba(97, 189, 242, 0.12)",
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
series: [
|
series: [
|
||||||
{
|
{
|
||||||
@@ -157,16 +197,16 @@ const trendOption = computed(() => {
|
|||||||
data: totalValues,
|
data: totalValues,
|
||||||
lineStyle: {
|
lineStyle: {
|
||||||
width: 3,
|
width: 3,
|
||||||
color: "#74ecff"
|
color: "#74ecff",
|
||||||
},
|
},
|
||||||
itemStyle: {
|
itemStyle: {
|
||||||
color: "#74ecff",
|
color: "#74ecff",
|
||||||
borderColor: "#081f3e",
|
borderColor: "#081f3e",
|
||||||
borderWidth: 2
|
borderWidth: 2,
|
||||||
},
|
},
|
||||||
areaStyle: {
|
areaStyle: {
|
||||||
color: "rgba(116, 236, 255, 0.12)"
|
color: "rgba(116, 236, 255, 0.12)",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "闭环告警",
|
name: "闭环告警",
|
||||||
@@ -177,44 +217,48 @@ const trendOption = computed(() => {
|
|||||||
data: closedValues,
|
data: closedValues,
|
||||||
lineStyle: {
|
lineStyle: {
|
||||||
width: 3,
|
width: 3,
|
||||||
color: "#62e0a8"
|
color: "#62e0a8",
|
||||||
},
|
},
|
||||||
itemStyle: {
|
itemStyle: {
|
||||||
color: "#62e0a8",
|
color: "#62e0a8",
|
||||||
borderColor: "#081f3e",
|
borderColor: "#081f3e",
|
||||||
borderWidth: 2
|
borderWidth: 2,
|
||||||
},
|
},
|
||||||
areaStyle: {
|
areaStyle: {
|
||||||
color: "rgba(98, 224, 168, 0.08)"
|
color: "rgba(98, 224, 168, 0.08)",
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const typeStats = computed(() => {
|
const typeStats = computed(() => {
|
||||||
const statsMap = /* @__PURE__ */ new Map();
|
const statsMap = new Map<string, number>();
|
||||||
reportData.forEach((item) => {
|
reportData.forEach((item) => {
|
||||||
statsMap.set(item.type, (statsMap.get(item.type) ?? 0) + 1);
|
statsMap.set(item.type, (statsMap.get(item.type) ?? 0) + 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
return Array.from(statsMap.entries()).map(([label, value]) => ({
|
return Array.from(statsMap.entries()).map(([label, value]) => ({
|
||||||
label,
|
label,
|
||||||
value
|
value,
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
const typeRadarOption = computed(() => {
|
|
||||||
|
const typeRadarOption = computed<EChartsOption>(() => {
|
||||||
const maxValue = Math.max(...typeStats.value.map((item) => item.value), 1);
|
const maxValue = Math.max(...typeStats.value.map((item) => item.value), 1);
|
||||||
const indicators = typeStats.value.map((item) => ({
|
const indicators = typeStats.value.map((item) => ({
|
||||||
name: item.label,
|
name: item.label,
|
||||||
max: maxValue
|
max: maxValue,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
tooltip: {
|
tooltip: {
|
||||||
trigger: "item",
|
trigger: "item",
|
||||||
backgroundColor: "rgba(6, 20, 42, 0.92)",
|
backgroundColor: "rgba(6, 20, 42, 0.92)",
|
||||||
borderColor: "rgba(96, 208, 255, 0.24)",
|
borderColor: "rgba(96, 208, 255, 0.24)",
|
||||||
textStyle: {
|
textStyle: {
|
||||||
color: "#eefcff"
|
color: "#eefcff",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
radar: {
|
radar: {
|
||||||
radius: "68%",
|
radius: "68%",
|
||||||
@@ -223,23 +267,23 @@ const typeRadarOption = computed(() => {
|
|||||||
splitNumber: 4,
|
splitNumber: 4,
|
||||||
axisName: {
|
axisName: {
|
||||||
color: "#dff7ff",
|
color: "#dff7ff",
|
||||||
fontSize: 12
|
fontSize: 12,
|
||||||
},
|
},
|
||||||
axisLine: {
|
axisLine: {
|
||||||
lineStyle: {
|
lineStyle: {
|
||||||
color: "rgba(97, 189, 242, 0.22)"
|
color: "rgba(97, 189, 242, 0.22)",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
splitLine: {
|
splitLine: {
|
||||||
lineStyle: {
|
lineStyle: {
|
||||||
color: "rgba(97, 189, 242, 0.16)"
|
color: "rgba(97, 189, 242, 0.16)",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
splitArea: {
|
splitArea: {
|
||||||
areaStyle: {
|
areaStyle: {
|
||||||
color: ["rgba(16, 52, 94, 0.10)", "rgba(16, 52, 94, 0.05)"]
|
color: ["rgba(16, 52, 94, 0.10)", "rgba(16, 52, 94, 0.05)"],
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
series: [
|
series: [
|
||||||
{
|
{
|
||||||
@@ -249,47 +293,50 @@ const typeRadarOption = computed(() => {
|
|||||||
value: typeStats.value.map((item) => item.value),
|
value: typeStats.value.map((item) => item.value),
|
||||||
name: "告警类型分布",
|
name: "告警类型分布",
|
||||||
areaStyle: {
|
areaStyle: {
|
||||||
color: "rgba(73, 207, 255, 0.20)"
|
color: "rgba(73, 207, 255, 0.20)",
|
||||||
},
|
},
|
||||||
lineStyle: {
|
lineStyle: {
|
||||||
color: "#61dcff",
|
color: "#61dcff",
|
||||||
width: 2.5
|
width: 2.5,
|
||||||
},
|
},
|
||||||
itemStyle: {
|
itemStyle: {
|
||||||
color: "#8ff3ff",
|
color: "#8ff3ff",
|
||||||
borderColor: "#07203e",
|
borderColor: "#07203e",
|
||||||
borderWidth: 2
|
borderWidth: 2,
|
||||||
},
|
},
|
||||||
symbolSize: 8
|
symbolSize: 8,
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const statusStats = computed(() => {
|
const statusStats = computed(() => {
|
||||||
const order = ["待研判", "待处理", "处置完成", "误判"];
|
const order: AlarmStatus[] = ["待研判", "待处理", "处置完成", "误判"];
|
||||||
const total = reportData.length || 1;
|
const total = reportData.length || 1;
|
||||||
|
|
||||||
return order.map((status) => {
|
return order.map((status) => {
|
||||||
const value = reportData.filter((item) => item.status === status).length;
|
const value = reportData.filter((item) => item.status === status).length;
|
||||||
return {
|
return {
|
||||||
label: status,
|
label: status,
|
||||||
value,
|
value,
|
||||||
percent: `${Math.round(value / total * 100)}%`
|
percent: `${Math.round((value / total) * 100)}%`,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
const statusPieOption = computed(() => ({
|
|
||||||
|
const statusPieOption = computed<EChartsOption>(() => ({
|
||||||
tooltip: {
|
tooltip: {
|
||||||
trigger: "item",
|
trigger: "item",
|
||||||
backgroundColor: "rgba(6, 20, 42, 0.92)",
|
backgroundColor: "rgba(6, 20, 42, 0.92)",
|
||||||
borderColor: "rgba(96, 208, 255, 0.24)",
|
borderColor: "rgba(96, 208, 255, 0.24)",
|
||||||
textStyle: {
|
textStyle: {
|
||||||
color: "#eefcff"
|
color: "#eefcff",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
legend: {
|
legend: {
|
||||||
show: false
|
show: false,
|
||||||
},
|
},
|
||||||
series: [
|
series: [
|
||||||
{
|
{
|
||||||
@@ -299,50 +346,63 @@ const statusPieOption = computed(() => ({
|
|||||||
avoidLabelOverlap: false,
|
avoidLabelOverlap: false,
|
||||||
itemStyle: {
|
itemStyle: {
|
||||||
borderColor: "#081f3e",
|
borderColor: "#081f3e",
|
||||||
borderWidth: 3
|
borderWidth: 3,
|
||||||
},
|
},
|
||||||
label: {
|
label: {
|
||||||
show: true,
|
show: true,
|
||||||
color: "#dff7ff",
|
color: "#dff7ff",
|
||||||
formatter: "{b}\n{d}%",
|
formatter: "{b}\n{d}%",
|
||||||
fontSize: 12
|
fontSize: 12,
|
||||||
},
|
},
|
||||||
labelLine: {
|
labelLine: {
|
||||||
lineStyle: {
|
lineStyle: {
|
||||||
color: "rgba(181, 230, 249, 0.52)"
|
color: "rgba(181, 230, 249, 0.52)",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
data: statusStats.value.map((item) => ({
|
data: statusStats.value.map((item) => ({
|
||||||
name: item.label,
|
name: item.label,
|
||||||
value: item.value,
|
value: item.value,
|
||||||
itemStyle: {
|
itemStyle: {
|
||||||
color: item.label === "待研判" ? "#ffd66e" : item.label === "待处理" ? "#ff8b78" : item.label === "处置完成" ? "#62e0a8" : "#8ea7ff"
|
color:
|
||||||
}
|
item.label === "待研判"
|
||||||
}))
|
? "#ffd66e"
|
||||||
}
|
: item.label === "待处理"
|
||||||
]
|
? "#ff8b78"
|
||||||
|
: item.label === "处置完成"
|
||||||
|
? "#62e0a8"
|
||||||
|
: "#8ea7ff",
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
],
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const projectDistribution = computed(() => {
|
const projectDistribution = computed(() => {
|
||||||
const now = /* @__PURE__ */ new Date("2026-06-23T23:59:59");
|
const now = new Date("2026-06-23T23:59:59");
|
||||||
const rangeDaysMap = {
|
const rangeDaysMap: Record<DistributionRange, number> = {
|
||||||
近一周: 7,
|
近一周: 7,
|
||||||
近一个月: 30,
|
近一个月: 30,
|
||||||
近三个月: 90
|
近三个月: 90,
|
||||||
};
|
};
|
||||||
const threshold = new Date(
|
const threshold = new Date(
|
||||||
now.getTime() - rangeDaysMap[distributionRange.value] * 24 * 60 * 60 * 1e3
|
now.getTime() - rangeDaysMap[distributionRange.value] * 24 * 60 * 60 * 1000,
|
||||||
);
|
);
|
||||||
const data = reportData.filter((item) => new Date(item.time.replace(" ", "T")) >= threshold);
|
const data = reportData.filter((item) => new Date(item.time.replace(" ", "T")) >= threshold);
|
||||||
const statsMap = /* @__PURE__ */ new Map();
|
const statsMap = new Map<string, number>();
|
||||||
|
|
||||||
data.forEach((item) => {
|
data.forEach((item) => {
|
||||||
statsMap.set(item.project, (statsMap.get(item.project) ?? 0) + 1);
|
statsMap.set(item.project, (statsMap.get(item.project) ?? 0) + 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
const maxValue = Math.max(...statsMap.values(), 1);
|
const maxValue = Math.max(...statsMap.values(), 1);
|
||||||
return Array.from(statsMap.entries()).map(([label, value]) => ({
|
|
||||||
|
return Array.from(statsMap.entries())
|
||||||
|
.map(([label, value]) => ({
|
||||||
label,
|
label,
|
||||||
value,
|
value,
|
||||||
width: `${value / maxValue * 100}%`
|
width: `${(value / maxValue) * 100}%`,
|
||||||
})).sort((a, b) => b.value - a.value);
|
}))
|
||||||
|
.sort((a, b) => b.value - a.value);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -419,7 +479,7 @@ const projectDistribution = computed(() => {
|
|||||||
:key="range"
|
:key="range"
|
||||||
:class="['report-filter-tab', { active: distributionRange === range }]"
|
:class="['report-filter-tab', { active: distributionRange === range }]"
|
||||||
type="button"
|
type="button"
|
||||||
@click="distributionRange = range"
|
@click="distributionRange = range as DistributionRange"
|
||||||
>
|
>
|
||||||
{{ range }}
|
{{ range }}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,21 @@
|
|||||||
<script setup>
|
<script lang="ts" setup>
|
||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
const patrolRecords = [
|
|
||||||
|
interface PatrolAlarmRecord {
|
||||||
|
id: string;
|
||||||
|
type: string;
|
||||||
|
reportTime: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PatrolRecord {
|
||||||
|
id: string;
|
||||||
|
robot: string;
|
||||||
|
startTime: string;
|
||||||
|
endTime: string;
|
||||||
|
alarms: PatrolAlarmRecord[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const patrolRecords: PatrolRecord[] = [
|
||||||
{
|
{
|
||||||
id: "PR-20260623-001",
|
id: "PR-20260623-001",
|
||||||
robot: "巡检机器人 R1",
|
robot: "巡检机器人 R1",
|
||||||
@@ -8,8 +23,8 @@ const patrolRecords = [
|
|||||||
endTime: "2026-06-23 09:02:15",
|
endTime: "2026-06-23 09:02:15",
|
||||||
alarms: [
|
alarms: [
|
||||||
{ id: "AL-001", type: "线缆破损", reportTime: "2026-06-23 08:36:12" },
|
{ id: "AL-001", type: "线缆破损", reportTime: "2026-06-23 08:36:12" },
|
||||||
{ id: "AL-002", type: "局部高温", reportTime: "2026-06-23 08:42:09" }
|
{ id: "AL-002", type: "局部高温", reportTime: "2026-06-23 08:42:09" },
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "PR-20260623-002",
|
id: "PR-20260623-002",
|
||||||
@@ -17,8 +32,8 @@ const patrolRecords = [
|
|||||||
startTime: "2026-06-23 09:20:08",
|
startTime: "2026-06-23 09:20:08",
|
||||||
endTime: "2026-06-23 10:01:46",
|
endTime: "2026-06-23 10:01:46",
|
||||||
alarms: [
|
alarms: [
|
||||||
{ id: "AL-005", type: "应急门开启异常", reportTime: "2026-06-23 09:48:57" }
|
{ id: "AL-005", type: "应急门开启异常", reportTime: "2026-06-23 09:48:57" },
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "PR-20260623-003",
|
id: "PR-20260623-003",
|
||||||
@@ -26,25 +41,28 @@ const patrolRecords = [
|
|||||||
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" },
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "PR-20260623-004",
|
id: "PR-20260623-004",
|
||||||
robot: "巡检机器人 R2",
|
robot: "巡检机器人 R2",
|
||||||
startTime: "2026-06-23 13:15:17",
|
startTime: "2026-06-23 13:15:17",
|
||||||
endTime: "2026-06-23 14:05:52",
|
endTime: "2026-06-23 14:05:52",
|
||||||
alarms: []
|
alarms: [],
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const dialogVisible = ref(false);
|
const dialogVisible = ref(false);
|
||||||
const activePatrolRecord = ref(null);
|
const activePatrolRecord = ref<PatrolRecord | null>(null);
|
||||||
function openAlarmDialog(record) {
|
|
||||||
|
function openAlarmDialog(record: PatrolRecord) {
|
||||||
activePatrolRecord.value = record;
|
activePatrolRecord.value = record;
|
||||||
dialogVisible.value = true;
|
dialogVisible.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeDialog() {
|
function closeDialog() {
|
||||||
dialogVisible.value = false;
|
dialogVisible.value = false;
|
||||||
activePatrolRecord.value = null;
|
activePatrolRecord.value = null;
|
||||||
|
|||||||
@@ -1,18 +1,75 @@
|
|||||||
<script setup>
|
<script lang="ts" setup>
|
||||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from "vue";
|
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from "vue";
|
||||||
import { getRosCarList } from "@/api/rosCar";
|
import { getRosCarList } from "@/api/rosCar";
|
||||||
|
|
||||||
|
type GridMode = 1 | 4 | 9;
|
||||||
|
type CameraType = "可见光" | "红外";
|
||||||
|
type DeviceStatus = "空闲" | "巡检" | "离线" | "充电中";
|
||||||
|
|
||||||
|
interface RosCarRecord {
|
||||||
|
id?: string;
|
||||||
|
deviceNo?: string;
|
||||||
|
name?: string;
|
||||||
|
batteryLevel?: number;
|
||||||
|
status?: number;
|
||||||
|
projectName?: string;
|
||||||
|
cameraWebrtcUrl?: string;
|
||||||
|
cameraNo?: string;
|
||||||
|
cameraWebrtcUrl2?: string;
|
||||||
|
cameraNo2?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RosCarListResult {
|
||||||
|
total?: number;
|
||||||
|
data?: RosCarRecord[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RosCarListResponse {
|
||||||
|
data?: RosCarListResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CameraItem {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
type: CameraType;
|
||||||
|
status: DeviceStatus;
|
||||||
|
url: string;
|
||||||
|
cameraNo?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DeviceNode {
|
||||||
|
id: string;
|
||||||
|
deviceNo?: string;
|
||||||
|
name: string;
|
||||||
|
project: string;
|
||||||
|
status: DeviceStatus;
|
||||||
|
battery: string;
|
||||||
|
cameras: CameraItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ProjectCameraItem extends CameraItem {
|
||||||
|
deviceId: string;
|
||||||
|
deviceName: string;
|
||||||
|
project: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SlotStreamState {
|
||||||
|
loading: boolean;
|
||||||
|
error: string;
|
||||||
|
}
|
||||||
|
|
||||||
const defaultProject = "视频中心";
|
const defaultProject = "视频中心";
|
||||||
const gridMode = ref(4);
|
const gridMode = ref<GridMode>(4);
|
||||||
const activeSlotIndex = ref(0);
|
const activeSlotIndex = ref(0);
|
||||||
const videoSlots = ref([]);
|
const videoSlots = ref<Array<ProjectCameraItem | null>>([]);
|
||||||
const slotVideoRefs = ref([]);
|
const slotVideoRefs = ref<Array<HTMLVideoElement | null>>([]);
|
||||||
const slotStreamStates = ref([]);
|
const slotStreamStates = ref<SlotStreamState[]>([]);
|
||||||
const videoGridBoardRef = ref(null);
|
const videoGridBoardRef = ref<HTMLElement | null>(null);
|
||||||
const videoGridBoardStyle = ref({});
|
const videoGridBoardStyle = ref<Record<string, string>>({});
|
||||||
const slotPeerConnections = /* @__PURE__ */ new Map();
|
const slotPeerConnections = new Map<number, RTCPeerConnection>();
|
||||||
const slotRequestIds = /* @__PURE__ */ new Map();
|
const slotRequestIds = new Map<number, number>();
|
||||||
let videoGridResizeObserver = null;
|
let videoGridResizeObserver: ResizeObserver | null = null;
|
||||||
const devices = ref([]);
|
const devices = ref<DeviceNode[]>([]);
|
||||||
const listLoading = ref(false);
|
const listLoading = ref(false);
|
||||||
const listError = ref("");
|
const listError = ref("");
|
||||||
const playbackDialogVisible = ref(false);
|
const playbackDialogVisible = ref(false);
|
||||||
@@ -20,107 +77,141 @@ const playbackStartAt = ref("2026-06-23T14:20");
|
|||||||
const playbackEndAt = ref("2026-06-23T14:50");
|
const playbackEndAt = ref("2026-06-23T14:50");
|
||||||
const playbackProgress = ref(0);
|
const playbackProgress = ref(0);
|
||||||
const playbackPaused = ref(true);
|
const playbackPaused = ref(true);
|
||||||
const playbackPlayerRef = ref(null);
|
const playbackPlayerRef = ref<HTMLVideoElement | null>(null);
|
||||||
const slotCountMap = {
|
|
||||||
|
const slotCountMap: Record<GridMode, number> = {
|
||||||
1: 1,
|
1: 1,
|
||||||
4: 4,
|
4: 4,
|
||||||
9: 9
|
9: 9,
|
||||||
};
|
};
|
||||||
|
|
||||||
const currentProjectName = computed(() => devices.value[0]?.project || defaultProject);
|
const currentProjectName = computed(() => devices.value[0]?.project || defaultProject);
|
||||||
const playableCameras = computed(
|
|
||||||
() => devices.value.flatMap(
|
const playableCameras = computed<ProjectCameraItem[]>(() =>
|
||||||
(device) => device.cameras.map((camera) => ({
|
devices.value.flatMap((device) =>
|
||||||
|
device.cameras.map((camera) => ({
|
||||||
...camera,
|
...camera,
|
||||||
deviceId: device.id,
|
deviceId: device.id,
|
||||||
deviceName: device.name,
|
deviceName: device.name,
|
||||||
project: device.project
|
project: device.project,
|
||||||
}))
|
})),
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
const cameraTotal = computed(() => playableCameras.value.length);
|
const cameraTotal = computed(() => playableCameras.value.length);
|
||||||
|
|
||||||
const visibleSlots = computed(() => {
|
const visibleSlots = computed(() => {
|
||||||
const targetCount = slotCountMap[gridMode.value];
|
const targetCount = slotCountMap[gridMode.value];
|
||||||
|
|
||||||
return Array.from(
|
return Array.from(
|
||||||
{ length: targetCount },
|
{ length: targetCount },
|
||||||
(_, index) => videoSlots.value[index] ?? null
|
(_, index) => videoSlots.value[index] ?? null,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const activeCamera = computed(() => visibleSlots.value[activeSlotIndex.value] ?? null);
|
const activeCamera = computed(() => visibleSlots.value[activeSlotIndex.value] ?? null);
|
||||||
|
|
||||||
const activeSlotLabel = computed(() => `画面 ${activeSlotIndex.value + 1}`);
|
const activeSlotLabel = computed(() => `画面 ${activeSlotIndex.value + 1}`);
|
||||||
|
|
||||||
const playbackRangeLabel = computed(() => {
|
const playbackRangeLabel = computed(() => {
|
||||||
const startLabel = playbackStartAt.value.replace("T", " ");
|
const startLabel = playbackStartAt.value.replace("T", " ");
|
||||||
const endLabel = playbackEndAt.value.replace("T", " ");
|
const endLabel = playbackEndAt.value.replace("T", " ");
|
||||||
return `${startLabel} - ${endLabel}`;
|
return `${startLabel} - ${endLabel}`;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 根据视频墙实际可用宽高计算宫格尺寸,保证每个画面都是 16:9 且不会溢出卡片
|
||||||
function updateVideoGridSize() {
|
function updateVideoGridSize() {
|
||||||
const board = videoGridBoardRef.value;
|
const board = videoGridBoardRef.value;
|
||||||
|
|
||||||
if (!board) {
|
if (!board) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const columns = Math.sqrt(gridMode.value);
|
const columns = Math.sqrt(gridMode.value);
|
||||||
const rows = columns;
|
const rows = columns;
|
||||||
const gap = 8;
|
const gap = 8;
|
||||||
const availableWidth = board.clientWidth - gap * (columns - 1);
|
const availableWidth = board.clientWidth - gap * (columns - 1);
|
||||||
const availableHeight = board.clientHeight - gap * (rows - 1);
|
const availableHeight = board.clientHeight - gap * (rows - 1);
|
||||||
const cellWidthByContainerWidth = availableWidth / columns;
|
const cellWidthByContainerWidth = availableWidth / columns;
|
||||||
const cellWidthByContainerHeight = availableHeight / rows * (16 / 9);
|
const cellWidthByContainerHeight = (availableHeight / rows) * (16 / 9);
|
||||||
const cellWidth = Math.max(0, Math.min(cellWidthByContainerWidth, cellWidthByContainerHeight));
|
const cellWidth = Math.max(0, Math.min(cellWidthByContainerWidth, cellWidthByContainerHeight));
|
||||||
const cellHeight = cellWidth * (9 / 16);
|
const cellHeight = cellWidth * (9 / 16);
|
||||||
|
|
||||||
videoGridBoardStyle.value = {
|
videoGridBoardStyle.value = {
|
||||||
gridTemplateColumns: `repeat(${columns}, ${cellWidth}px)`,
|
gridTemplateColumns: `repeat(${columns}, ${cellWidth}px)`,
|
||||||
gridAutoRows: `${cellHeight}px`
|
gridAutoRows: `${cellHeight}px`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 根据当前宫格数量补齐画面槽位,切换布局时保留已播放的视频
|
||||||
function ensureVideoSlotCount() {
|
function ensureVideoSlotCount() {
|
||||||
const targetCount = slotCountMap[gridMode.value];
|
const targetCount = slotCountMap[gridMode.value];
|
||||||
const previousCount = videoSlots.value.length;
|
const previousCount = videoSlots.value.length;
|
||||||
|
|
||||||
for (let index = targetCount; index < previousCount; index += 1) {
|
for (let index = targetCount; index < previousCount; index += 1) {
|
||||||
stopSlotStream(index);
|
stopSlotStream(index);
|
||||||
}
|
}
|
||||||
|
|
||||||
videoSlots.value = Array.from(
|
videoSlots.value = Array.from(
|
||||||
{ length: targetCount },
|
{ length: targetCount },
|
||||||
(_, index) => videoSlots.value[index] ?? null
|
(_, index) => videoSlots.value[index] ?? null,
|
||||||
);
|
);
|
||||||
slotVideoRefs.value = Array.from(
|
slotVideoRefs.value = Array.from(
|
||||||
{ length: targetCount },
|
{ length: targetCount },
|
||||||
(_, index) => slotVideoRefs.value[index] ?? null
|
(_, index) => slotVideoRefs.value[index] ?? null,
|
||||||
);
|
);
|
||||||
slotStreamStates.value = Array.from(
|
slotStreamStates.value = Array.from(
|
||||||
{ length: targetCount },
|
{ length: targetCount },
|
||||||
(_, index) => slotStreamStates.value[index] ?? { loading: false, error: "" }
|
(_, index) => slotStreamStates.value[index] ?? { loading: false, error: "" },
|
||||||
);
|
);
|
||||||
|
|
||||||
if (activeSlotIndex.value >= targetCount) {
|
if (activeSlotIndex.value >= targetCount) {
|
||||||
activeSlotIndex.value = targetCount - 1;
|
activeSlotIndex.value = targetCount - 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function normalizeDeviceStatus(status) {
|
|
||||||
const statusMap = {
|
// 将接口的小车状态码转换为页面展示状态
|
||||||
|
function normalizeDeviceStatus(status?: number): DeviceStatus {
|
||||||
|
const statusMap: Record<number, DeviceStatus> = {
|
||||||
0: "空闲",
|
0: "空闲",
|
||||||
1: "巡检",
|
1: "巡检",
|
||||||
2: "离线",
|
2: "离线",
|
||||||
3: "充电中"
|
3: "充电中",
|
||||||
};
|
};
|
||||||
|
|
||||||
return statusMap[status ?? 2] ?? "离线";
|
return statusMap[status ?? 2] ?? "离线";
|
||||||
}
|
}
|
||||||
function buildCameraItem(car, deviceId, type, url, cameraNo) {
|
|
||||||
|
// 根据小车视频地址生成可播放的相机节点
|
||||||
|
function buildCameraItem(
|
||||||
|
car: RosCarRecord,
|
||||||
|
deviceId: string,
|
||||||
|
type: CameraType,
|
||||||
|
url?: string,
|
||||||
|
cameraNo?: string,
|
||||||
|
): CameraItem | null {
|
||||||
if (!url) {
|
if (!url) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: `${deviceId}-${type}`,
|
id: `${deviceId}-${type}`,
|
||||||
name: `${type}相机`,
|
name: `${type}相机`,
|
||||||
type,
|
type,
|
||||||
status: normalizeDeviceStatus(car.status),
|
status: normalizeDeviceStatus(car.status),
|
||||||
url,
|
url,
|
||||||
cameraNo
|
cameraNo,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
function normalizeDeviceNode(car, index) {
|
|
||||||
|
// 将设备列表接口的小车记录转换为左侧机器人树节点
|
||||||
|
function normalizeDeviceNode(car: RosCarRecord, index: number): DeviceNode {
|
||||||
const deviceId = car.id ?? `ROBOT-${index + 1}`;
|
const deviceId = car.id ?? `ROBOT-${index + 1}`;
|
||||||
const cameras = [
|
const cameras = [
|
||||||
buildCameraItem(car, deviceId, "可见光", car.cameraWebrtcUrl, car.cameraNo),
|
buildCameraItem(car, deviceId, "可见光", car.cameraWebrtcUrl, car.cameraNo),
|
||||||
buildCameraItem(car, deviceId, "红外", car.cameraWebrtcUrl2, car.cameraNo2)
|
buildCameraItem(car, deviceId, "红外", car.cameraWebrtcUrl2, car.cameraNo2),
|
||||||
].filter((camera) => Boolean(camera));
|
].filter((camera): camera is CameraItem => Boolean(camera));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: deviceId,
|
id: deviceId,
|
||||||
deviceNo: car.deviceNo,
|
deviceNo: car.deviceNo,
|
||||||
@@ -128,19 +219,23 @@ function normalizeDeviceNode(car, index) {
|
|||||||
project: car.projectName || defaultProject,
|
project: car.projectName || defaultProject,
|
||||||
status: normalizeDeviceStatus(car.status),
|
status: normalizeDeviceStatus(car.status),
|
||||||
battery: typeof car.batteryLevel === "number" ? `${car.batteryLevel}%` : "--",
|
battery: typeof car.batteryLevel === "number" ? `${car.batteryLevel}%` : "--",
|
||||||
cameras
|
cameras,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 请求设备列表接口,加载左侧机器人及其可见光/红外相机
|
||||||
async function fetchDeviceList() {
|
async function fetchDeviceList() {
|
||||||
listLoading.value = true;
|
listLoading.value = true;
|
||||||
listError.value = "";
|
listError.value = "";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await getRosCarList({
|
const response = (await getRosCarList({
|
||||||
page: 1,
|
page: 1,
|
||||||
limit: 999,
|
limit: 999,
|
||||||
query: {}
|
query: {},
|
||||||
});
|
})) as RosCarListResponse;
|
||||||
const rows = response.data?.data ?? [];
|
const rows = response.data?.data ?? [];
|
||||||
|
|
||||||
devices.value = rows.map((car, index) => normalizeDeviceNode(car, index));
|
devices.value = rows.map((car, index) => normalizeDeviceNode(car, index));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
listError.value = "设备列表加载失败";
|
listError.value = "设备列表加载失败";
|
||||||
@@ -149,112 +244,146 @@ async function fetchDeviceList() {
|
|||||||
listLoading.value = false;
|
listLoading.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function selectVideoSlot(index) {
|
|
||||||
|
// 选中右侧视频墙中的目标画面
|
||||||
|
function selectVideoSlot(index: number) {
|
||||||
activeSlotIndex.value = index;
|
activeSlotIndex.value = index;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 停止全部画面的 WebRTC 拉流,用于页面卸载时释放资源
|
||||||
function stopAllSlotStreams() {
|
function stopAllSlotStreams() {
|
||||||
videoSlots.value.forEach((_, index) => stopSlotStream(index));
|
videoSlots.value.forEach((_, index) => stopSlotStream(index));
|
||||||
}
|
}
|
||||||
function stopSlotStream(index, clearState = true) {
|
|
||||||
|
// 停止指定画面的 WebRTC 拉流并释放播放器资源
|
||||||
|
function stopSlotStream(index: number, clearState = true) {
|
||||||
slotRequestIds.set(index, (slotRequestIds.get(index) ?? 0) + 1);
|
slotRequestIds.set(index, (slotRequestIds.get(index) ?? 0) + 1);
|
||||||
|
|
||||||
if (clearState) {
|
if (clearState) {
|
||||||
slotStreamStates.value[index] = { loading: false, error: "" };
|
slotStreamStates.value[index] = { loading: false, error: "" };
|
||||||
}
|
}
|
||||||
|
|
||||||
const peerConnection = slotPeerConnections.get(index);
|
const peerConnection = slotPeerConnections.get(index);
|
||||||
if (peerConnection) {
|
if (peerConnection) {
|
||||||
peerConnection.close();
|
peerConnection.close();
|
||||||
slotPeerConnections.delete(index);
|
slotPeerConnections.delete(index);
|
||||||
}
|
}
|
||||||
|
|
||||||
const video = slotVideoRefs.value[index];
|
const video = slotVideoRefs.value[index];
|
||||||
if (video) {
|
if (video) {
|
||||||
video.srcObject = null;
|
video.srcObject = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function buildWebrtcApiUrl(streamUrl) {
|
|
||||||
|
// 将设备返回的 webrtc 播放地址转换为 ZLMediaKit WebRTC 播放接口地址
|
||||||
|
function buildWebrtcApiUrl(streamUrl: string) {
|
||||||
const normalizedUrl = streamUrl.replace(/^webrtc:\/\//, "https://");
|
const normalizedUrl = streamUrl.replace(/^webrtc:\/\//, "https://");
|
||||||
const url = new URL(normalizedUrl);
|
const url = new URL(normalizedUrl);
|
||||||
|
|
||||||
return `${url.origin}${url.pathname}${url.search}`;
|
return `${url.origin}${url.pathname}${url.search}`;
|
||||||
}
|
}
|
||||||
function waitForIceGatheringComplete(peerConnection) {
|
|
||||||
|
// 等待本地 ICE 候选收集完成,确保发给流媒体服务的 offer 信息完整
|
||||||
|
function waitForIceGatheringComplete(peerConnection: RTCPeerConnection) {
|
||||||
if (peerConnection.iceGatheringState === "complete") {
|
if (peerConnection.iceGatheringState === "complete") {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
return new Promise((resolve) => {
|
|
||||||
|
return new Promise<void>((resolve) => {
|
||||||
const handleIceGatheringStateChange = () => {
|
const handleIceGatheringStateChange = () => {
|
||||||
if (peerConnection.iceGatheringState === "complete") {
|
if (peerConnection.iceGatheringState === "complete") {
|
||||||
peerConnection.removeEventListener("icegatheringstatechange", handleIceGatheringStateChange);
|
peerConnection.removeEventListener("icegatheringstatechange", handleIceGatheringStateChange);
|
||||||
resolve();
|
resolve();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
peerConnection.addEventListener("icegatheringstatechange", handleIceGatheringStateChange);
|
peerConnection.addEventListener("icegatheringstatechange", handleIceGatheringStateChange);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
function setSlotVideoRef(element, index) {
|
|
||||||
|
// 收集视频墙每个画面对应的 video 元素,供 WebRTC ontrack 写入媒体流
|
||||||
|
function setSlotVideoRef(element: Element | { $el?: Element } | null, index: number) {
|
||||||
const target = element instanceof Element ? element : element?.$el;
|
const target = element instanceof Element ? element : element?.$el;
|
||||||
slotVideoRefs.value[index] = target instanceof HTMLVideoElement ? target : null;
|
slotVideoRefs.value[index] = target instanceof HTMLVideoElement ? target : null;
|
||||||
}
|
}
|
||||||
async function playSlotStream(index, camera) {
|
|
||||||
|
// 通过 WebRTC offer/answer 将指定相机拉流到选中的画面槽位
|
||||||
|
async function playSlotStream(index: number, camera: ProjectCameraItem) {
|
||||||
stopSlotStream(index);
|
stopSlotStream(index);
|
||||||
|
|
||||||
if (!window.RTCPeerConnection) {
|
if (!window.RTCPeerConnection) {
|
||||||
slotStreamStates.value[index] = { loading: false, error: "当前浏览器不支持 WebRTC 播放" };
|
slotStreamStates.value[index] = { loading: false, error: "当前浏览器不支持 WebRTC 播放" };
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const video = slotVideoRefs.value[index];
|
const video = slotVideoRefs.value[index];
|
||||||
if (!video) {
|
if (!video) {
|
||||||
slotStreamStates.value[index] = { loading: false, error: "播放器未准备好" };
|
slotStreamStates.value[index] = { loading: false, error: "播放器未准备好" };
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const requestId = slotRequestIds.get(index) ?? 0;
|
const requestId = slotRequestIds.get(index) ?? 0;
|
||||||
const peerConnection = new RTCPeerConnection();
|
const peerConnection = new RTCPeerConnection();
|
||||||
slotPeerConnections.set(index, peerConnection);
|
slotPeerConnections.set(index, peerConnection);
|
||||||
slotStreamStates.value[index] = { loading: true, error: "" };
|
slotStreamStates.value[index] = { loading: true, error: "" };
|
||||||
|
|
||||||
peerConnection.addTransceiver("video", { direction: "recvonly" });
|
peerConnection.addTransceiver("video", { direction: "recvonly" });
|
||||||
peerConnection.addTransceiver("audio", { direction: "recvonly" });
|
peerConnection.addTransceiver("audio", { direction: "recvonly" });
|
||||||
peerConnection.ontrack = (event) => {
|
peerConnection.ontrack = (event) => {
|
||||||
if (requestId !== slotRequestIds.get(index)) {
|
if (requestId !== slotRequestIds.get(index)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
video.srcObject = event.streams[0];
|
video.srcObject = event.streams[0];
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const offer = await peerConnection.createOffer();
|
const offer = await peerConnection.createOffer();
|
||||||
await peerConnection.setLocalDescription(offer);
|
await peerConnection.setLocalDescription(offer);
|
||||||
await waitForIceGatheringComplete(peerConnection);
|
await waitForIceGatheringComplete(peerConnection);
|
||||||
|
|
||||||
const response = await fetch(buildWebrtcApiUrl(camera.url), {
|
const response = await fetch(buildWebrtcApiUrl(camera.url), {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/sdp"
|
"Content-Type": "application/sdp",
|
||||||
},
|
},
|
||||||
body: peerConnection.localDescription?.sdp ?? offer.sdp
|
body: peerConnection.localDescription?.sdp ?? offer.sdp,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`拉流失败\:${response.status}`);
|
throw new Error(`拉流失败:${response.status}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const responseText = await response.text();
|
const responseText = await response.text();
|
||||||
let answer = responseText;
|
let answer = responseText;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const responseData = JSON.parse(responseText);
|
const responseData = JSON.parse(responseText) as { code?: number; sdp?: string; msg?: string };
|
||||||
|
|
||||||
if (responseData.code && responseData.code !== 0) {
|
if (responseData.code && responseData.code !== 0) {
|
||||||
throw new Error(responseData.msg || "视频流拉取失败");
|
throw new Error(responseData.msg || "视频流拉取失败");
|
||||||
}
|
}
|
||||||
|
|
||||||
answer = responseData.sdp || responseText;
|
answer = responseData.sdp || responseText;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (responseText.trim().startsWith("{")) {
|
if (responseText.trim().startsWith("{")) {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (requestId !== slotRequestIds.get(index)) {
|
if (requestId !== slotRequestIds.get(index)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await peerConnection.setRemoteDescription({
|
await peerConnection.setRemoteDescription({
|
||||||
type: "answer",
|
type: "answer",
|
||||||
sdp: answer
|
sdp: answer,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (requestId === slotRequestIds.get(index)) {
|
if (requestId === slotRequestIds.get(index)) {
|
||||||
slotStreamStates.value[index] = {
|
slotStreamStates.value[index] = {
|
||||||
loading: false,
|
loading: false,
|
||||||
error: error instanceof Error ? error.message : "视频流拉取失败"
|
error: error instanceof Error ? error.message : "视频流拉取失败",
|
||||||
};
|
};
|
||||||
stopSlotStream(index, false);
|
stopSlotStream(index, false);
|
||||||
}
|
}
|
||||||
@@ -262,98 +391,135 @@ async function playSlotStream(index, camera) {
|
|||||||
if (requestId === slotRequestIds.get(index)) {
|
if (requestId === slotRequestIds.get(index)) {
|
||||||
slotStreamStates.value[index] = {
|
slotStreamStates.value[index] = {
|
||||||
...slotStreamStates.value[index],
|
...slotStreamStates.value[index],
|
||||||
loading: false
|
loading: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function closeVideoSlot(index) {
|
|
||||||
|
// 关闭指定画面并释放对应 WebRTC 拉流资源
|
||||||
|
function closeVideoSlot(index: number) {
|
||||||
stopSlotStream(index);
|
stopSlotStream(index);
|
||||||
videoSlots.value[index] = null;
|
videoSlots.value[index] = null;
|
||||||
|
|
||||||
if (activeSlotIndex.value === index) {
|
if (activeSlotIndex.value === index) {
|
||||||
activeSlotIndex.value = index;
|
activeSlotIndex.value = index;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async function playCamera(camera) {
|
|
||||||
|
// 将左侧相机播放到当前选中的右侧画面
|
||||||
|
async function playCamera(camera: ProjectCameraItem) {
|
||||||
const targetIndex = activeSlotIndex.value;
|
const targetIndex = activeSlotIndex.value;
|
||||||
videoSlots.value[targetIndex] = camera;
|
videoSlots.value[targetIndex] = camera;
|
||||||
|
|
||||||
await nextTick();
|
await nextTick();
|
||||||
await playSlotStream(targetIndex, camera);
|
await playSlotStream(targetIndex, camera);
|
||||||
}
|
}
|
||||||
async function setGridMode(mode) {
|
|
||||||
|
// 切换视频墙宫格布局
|
||||||
|
async function setGridMode(mode: GridMode) {
|
||||||
gridMode.value = mode;
|
gridMode.value = mode;
|
||||||
await nextTick();
|
await nextTick();
|
||||||
updateVideoGridSize();
|
updateVideoGridSize();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 同步录像回放播放器进度和暂停状态
|
||||||
function syncPlaybackProgress() {
|
function syncPlaybackProgress() {
|
||||||
const video = playbackPlayerRef.value;
|
const video = playbackPlayerRef.value;
|
||||||
|
|
||||||
if (!video) {
|
if (!video) {
|
||||||
playbackProgress.value = 0;
|
playbackProgress.value = 0;
|
||||||
playbackPaused.value = true;
|
playbackPaused.value = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const duration = Number.isFinite(video.duration) ? video.duration : 0;
|
const duration = Number.isFinite(video.duration) ? video.duration : 0;
|
||||||
playbackProgress.value = duration > 0 ? video.currentTime / duration * 100 : 0;
|
playbackProgress.value = duration > 0 ? (video.currentTime / duration) * 100 : 0;
|
||||||
playbackPaused.value = video.paused;
|
playbackPaused.value = video.paused;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 从头开始播放当前回放视频
|
||||||
function startPlayback() {
|
function startPlayback() {
|
||||||
const video = playbackPlayerRef.value;
|
const video = playbackPlayerRef.value;
|
||||||
|
|
||||||
if (!video) {
|
if (!video) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
video.currentTime = 0;
|
video.currentTime = 0;
|
||||||
void video.play();
|
void video.play();
|
||||||
playbackPaused.value = false;
|
playbackPaused.value = false;
|
||||||
syncPlaybackProgress();
|
syncPlaybackProgress();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 打开当前画面的录像回放弹窗
|
||||||
async function openPlaybackDialog() {
|
async function openPlaybackDialog() {
|
||||||
if (!activeCamera.value) {
|
if (!activeCamera.value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
playbackDialogVisible.value = true;
|
playbackDialogVisible.value = true;
|
||||||
playbackProgress.value = 0;
|
playbackProgress.value = 0;
|
||||||
playbackPaused.value = true;
|
playbackPaused.value = true;
|
||||||
|
|
||||||
await nextTick();
|
await nextTick();
|
||||||
startPlayback();
|
startPlayback();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 关闭录像回放弹窗并暂停播放器
|
||||||
function closePlaybackDialog() {
|
function closePlaybackDialog() {
|
||||||
playbackPlayerRef.value?.pause();
|
playbackPlayerRef.value?.pause();
|
||||||
playbackDialogVisible.value = false;
|
playbackDialogVisible.value = false;
|
||||||
playbackPaused.value = true;
|
playbackPaused.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 切换录像回放的播放和暂停状态
|
||||||
function togglePlaybackPause() {
|
function togglePlaybackPause() {
|
||||||
const video = playbackPlayerRef.value;
|
const video = playbackPlayerRef.value;
|
||||||
|
|
||||||
if (!video) {
|
if (!video) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (video.paused) {
|
if (video.paused) {
|
||||||
void video.play();
|
void video.play();
|
||||||
} else {
|
} else {
|
||||||
video.pause();
|
video.pause();
|
||||||
}
|
}
|
||||||
|
|
||||||
syncPlaybackProgress();
|
syncPlaybackProgress();
|
||||||
}
|
}
|
||||||
function seekPlayback(offsetSeconds) {
|
|
||||||
|
// 按秒数快进或快退录像回放
|
||||||
|
function seekPlayback(offsetSeconds: number) {
|
||||||
const video = playbackPlayerRef.value;
|
const video = playbackPlayerRef.value;
|
||||||
|
|
||||||
if (!video) {
|
if (!video) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const duration = Number.isFinite(video.duration) ? video.duration : 0;
|
const duration = Number.isFinite(video.duration) ? video.duration : 0;
|
||||||
const targetTime = Math.max(0, Math.min(duration, video.currentTime + offsetSeconds));
|
const targetTime = Math.max(0, Math.min(duration, video.currentTime + offsetSeconds));
|
||||||
video.currentTime = targetTime;
|
video.currentTime = targetTime;
|
||||||
syncPlaybackProgress();
|
syncPlaybackProgress();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 根据拖动条位置更新录像回放播放进度
|
||||||
function updatePlaybackProgress() {
|
function updatePlaybackProgress() {
|
||||||
const video = playbackPlayerRef.value;
|
const video = playbackPlayerRef.value;
|
||||||
|
|
||||||
if (!video) {
|
if (!video) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const duration = Number.isFinite(video.duration) ? video.duration : 0;
|
const duration = Number.isFinite(video.duration) ? video.duration : 0;
|
||||||
if (duration <= 0) {
|
if (duration <= 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
video.currentTime = playbackProgress.value / 100 * duration;
|
|
||||||
|
video.currentTime = (playbackProgress.value / 100) * duration;
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(gridMode, async () => {
|
watch(gridMode, async () => {
|
||||||
ensureVideoSlotCount();
|
ensureVideoSlotCount();
|
||||||
await nextTick();
|
await nextTick();
|
||||||
@@ -362,6 +528,7 @@ watch(gridMode, async () => {
|
|||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
void fetchDeviceList();
|
void fetchDeviceList();
|
||||||
updateVideoGridSize();
|
updateVideoGridSize();
|
||||||
|
|
||||||
if (videoGridBoardRef.value) {
|
if (videoGridBoardRef.value) {
|
||||||
videoGridResizeObserver = new ResizeObserver(updateVideoGridSize);
|
videoGridResizeObserver = new ResizeObserver(updateVideoGridSize);
|
||||||
videoGridResizeObserver.observe(videoGridBoardRef.value);
|
videoGridResizeObserver.observe(videoGridBoardRef.value);
|
||||||
@@ -451,7 +618,7 @@ onBeforeUnmount(() => {
|
|||||||
:key="mode"
|
:key="mode"
|
||||||
:class="['grid-switch-btn', { active: gridMode === mode }]"
|
:class="['grid-switch-btn', { active: gridMode === mode }]"
|
||||||
type="button"
|
type="button"
|
||||||
@click="setGridMode(mode)"
|
@click="setGridMode(mode as GridMode)"
|
||||||
>
|
>
|
||||||
{{ mode }} 宫格
|
{{ mode }} 宫格
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -1,19 +1,27 @@
|
|||||||
function createAlertPhoto(scene, accent) {
|
export type AlertPhotoScene = "cable" | "heat" | "crack";
|
||||||
const sceneMarkup = scene === "cable" ? `
|
|
||||||
|
export function createAlertPhoto(scene: AlertPhotoScene, accent: string) {
|
||||||
|
const sceneMarkup =
|
||||||
|
scene === "cable"
|
||||||
|
? `
|
||||||
<path d="M64 112 C140 90, 222 88, 304 104 S470 132, 556 110" stroke="${accent}" stroke-width="6" fill="none" stroke-linecap="round" opacity="0.9" />
|
<path d="M64 112 C140 90, 222 88, 304 104 S470 132, 556 110" stroke="${accent}" stroke-width="6" fill="none" stroke-linecap="round" opacity="0.9" />
|
||||||
<path d="M314 106 L340 92 L366 120" stroke="#ffe9a6" stroke-width="5" fill="none" stroke-linecap="round" />
|
<path d="M314 106 L340 92 L366 120" stroke="#ffe9a6" stroke-width="5" fill="none" stroke-linecap="round" />
|
||||||
<circle cx="340" cy="106" r="32" fill="rgba(255,102,82,0.18)" stroke="#ff8e79" stroke-width="2" stroke-dasharray="6 6" />
|
<circle cx="340" cy="106" r="32" fill="rgba(255,102,82,0.18)" stroke="#ff8e79" stroke-width="2" stroke-dasharray="6 6" />
|
||||||
` : scene === "heat" ? `
|
`
|
||||||
|
: scene === "heat"
|
||||||
|
? `
|
||||||
<ellipse cx="340" cy="122" rx="84" ry="46" fill="rgba(255,122,68,0.26)" />
|
<ellipse cx="340" cy="122" rx="84" ry="46" fill="rgba(255,122,68,0.26)" />
|
||||||
<ellipse cx="344" cy="122" rx="58" ry="30" fill="rgba(255,193,82,0.28)" />
|
<ellipse cx="344" cy="122" rx="58" ry="30" fill="rgba(255,193,82,0.28)" />
|
||||||
<path d="M320 146 C330 126, 334 114, 332 96 C344 112, 350 126, 348 148" fill="none" stroke="#ffd36a" stroke-width="4" stroke-linecap="round" />
|
<path d="M320 146 C330 126, 334 114, 332 96 C344 112, 350 126, 348 148" fill="none" stroke="#ffd36a" stroke-width="4" stroke-linecap="round" />
|
||||||
<path d="M358 142 C366 126, 372 114, 370 100 C382 112, 388 126, 384 144" fill="none" stroke="#ff8a5b" stroke-width="4" stroke-linecap="round" />
|
<path d="M358 142 C366 126, 372 114, 370 100 C382 112, 388 126, 384 144" fill="none" stroke="#ff8a5b" stroke-width="4" stroke-linecap="round" />
|
||||||
` : `
|
`
|
||||||
|
: `
|
||||||
<path d="M206 88 L234 130 L226 166 L268 210 L254 248 L294 282" stroke="#d8ecff" stroke-width="4" fill="none" stroke-linecap="round" />
|
<path d="M206 88 L234 130 L226 166 L268 210 L254 248 L294 282" stroke="#d8ecff" stroke-width="4" fill="none" stroke-linecap="round" />
|
||||||
<path d="M248 152 L280 136 L306 164" stroke="#d8ecff" stroke-width="3" fill="none" stroke-linecap="round" />
|
<path d="M248 152 L280 136 L306 164" stroke="#d8ecff" stroke-width="3" fill="none" stroke-linecap="round" />
|
||||||
<path d="M266 212 L302 196 L328 220" stroke="#d8ecff" stroke-width="3" fill="none" stroke-linecap="round" />
|
<path d="M266 212 L302 196 L328 220" stroke="#d8ecff" stroke-width="3" fill="none" stroke-linecap="round" />
|
||||||
<ellipse cx="258" cy="178" rx="86" ry="118" fill="rgba(132,196,255,0.08)" stroke="${accent}" stroke-width="2" stroke-dasharray="5 7" />
|
<ellipse cx="258" cy="178" rx="86" ry="118" fill="rgba(132,196,255,0.08)" stroke="${accent}" stroke-width="2" stroke-dasharray="5 7" />
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const svg = `
|
const svg = `
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 360">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 360">
|
||||||
<defs>
|
<defs>
|
||||||
@@ -53,22 +61,21 @@ function createAlertPhoto(scene, accent) {
|
|||||||
<rect x="20" y="20" width="600" height="320" rx="16" fill="none" stroke="rgba(110,224,255,0.12)" stroke-width="2" />
|
<rect x="20" y="20" width="600" height="320" rx="16" fill="none" stroke="rgba(110,224,255,0.12)" stroke-width="2" />
|
||||||
</svg>
|
</svg>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
return `data:image/svg+xml;charset=UTF-8,${encodeURIComponent(svg)}`;
|
return `data:image/svg+xml;charset=UTF-8,${encodeURIComponent(svg)}`;
|
||||||
}
|
}
|
||||||
const recentAlertPhotoSources = {
|
|
||||||
|
export const recentAlertPhotoSources = {
|
||||||
"线缆破损": createAlertPhoto("cable", "#ff8f79"),
|
"线缆破损": createAlertPhoto("cable", "#ff8f79"),
|
||||||
"电缆表面破损": createAlertPhoto("cable", "#ff8f79"),
|
"电缆表面破损": createAlertPhoto("cable", "#ff8f79"),
|
||||||
"桥架断裂": 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;
|
||||||
const fallbackAlertPhoto = recentAlertPhotoSources["墙体裂缝"];
|
|
||||||
function getRecentAlertPhoto(type) {
|
const fallbackAlertPhoto = recentAlertPhotoSources["墙体渗漏"];
|
||||||
return recentAlertPhotoSources[type] ?? fallbackAlertPhoto;
|
|
||||||
|
export function getRecentAlertPhoto(type: string) {
|
||||||
|
return recentAlertPhotoSources[type as keyof typeof recentAlertPhotoSources] ?? fallbackAlertPhoto;
|
||||||
}
|
}
|
||||||
export {
|
|
||||||
createAlertPhoto,
|
|
||||||
getRecentAlertPhoto,
|
|
||||||
recentAlertPhotoSources
|
|
||||||
};
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup>
|
<script lang="ts" setup>
|
||||||
|
import SwitchPageBtn from "@/views/dashboard1/components/btn";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
import { defineComponent } from "vue";
|
|
||||||
import "./index.less";
|
|
||||||
const longIndex = [0, 6, 12];
|
|
||||||
export default defineComponent({
|
|
||||||
name: "Chart",
|
|
||||||
props: {},
|
|
||||||
setup(props) {
|
|
||||||
return () => <div class="chart">
|
|
||||||
<ul class="scale__ul">{Array.from({ length: 13 }).map((_, index) => {
|
|
||||||
const long = longIndex.includes(index);
|
|
||||||
return <li
|
|
||||||
class={[`scale__li`, { "scale__li--long": long }, { "scale__li--active": index > 5 }]}
|
|
||||||
style={{ transform: `rotate(-${15 * index}deg) translate(${long ? 59 : 65}px,-50%)` }}
|
|
||||||
key={index}
|
|
||||||
/>;
|
|
||||||
})}</ul>
|
|
||||||
<div class="chart__name">xx</div>
|
|
||||||
</div>;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
26
src/views/dashboard3/components/chart/index.tsx
Normal file
26
src/views/dashboard3/components/chart/index.tsx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import {defineComponent} from "vue";
|
||||||
|
import "./index.less"
|
||||||
|
|
||||||
|
const longIndex = [0, 6, 12];
|
||||||
|
export default defineComponent({
|
||||||
|
name: "Chart",
|
||||||
|
props: {},
|
||||||
|
setup(props) {
|
||||||
|
return () => (
|
||||||
|
<div class='chart'>
|
||||||
|
<ul class='scale__ul'>
|
||||||
|
{
|
||||||
|
Array.from({length: 13}).map((_, index) => {
|
||||||
|
const long = longIndex.includes(index);
|
||||||
|
return <li
|
||||||
|
class={[`scale__li`, {'scale__li--long': long}, {'scale__li--active': index > 5}]}
|
||||||
|
style={{transform: `rotate(-${15 * index}deg) translate(${long ? 59 : 65}px,-50%)`}}
|
||||||
|
key={index}></li>
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
<div class='chart__name'>xx</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -1,16 +1,20 @@
|
|||||||
<script setup>
|
<script lang="ts" setup>
|
||||||
import { computed, ref } from "vue";
|
import { computed, ref } from "vue";
|
||||||
|
|
||||||
const animationSvgRef = ref();
|
const animationSvgRef = ref();
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
count: 16,
|
count: 16,
|
||||||
initR: 2,
|
initR: 2,
|
||||||
gap: 0.2,
|
gap: 0.2,
|
||||||
dur: 8,
|
dur: 8,
|
||||||
startY: 266.5
|
startY: 266.5,
|
||||||
};
|
};
|
||||||
|
|
||||||
function generateNumber() {
|
function generateNumber() {
|
||||||
return (Math.random() * 10).toFixed(1) + "Mb/s";
|
return (Math.random() * 10).toFixed(1) + "Mb/s";
|
||||||
}
|
}
|
||||||
|
|
||||||
const polylineComputed = computed(() => {
|
const polylineComputed = computed(() => {
|
||||||
const arr = [];
|
const arr = [];
|
||||||
let startY = config.startY;
|
let startY = config.startY;
|
||||||
@@ -18,25 +22,28 @@ const polylineComputed = computed(() => {
|
|||||||
let startX = 465;
|
let startX = 465;
|
||||||
let endX = 1820;
|
let endX = 1820;
|
||||||
const points = `${startX},${startY} 846,${startY} 1280,634 ${endX},634`;
|
const points = `${startX},${startY} 846,${startY} 1280,634 ${endX},634`;
|
||||||
|
|
||||||
const textY = startY - 29.5;
|
const textY = startY - 29.5;
|
||||||
const text1 = { x: startX + 137, y: textY, value: `${generateNumber()}` };
|
const text1 = { x: startX + 137, y: textY, value: `${generateNumber()}` };
|
||||||
const text2 = {
|
const text2 = {
|
||||||
x: startX + 137 + 147,
|
x: startX + 137 + 147,
|
||||||
y: textY,
|
y: textY,
|
||||||
value: `${generateNumber()}`
|
value: `${generateNumber()}`,
|
||||||
};
|
};
|
||||||
const text3 = {};
|
const text3 = {};
|
||||||
|
|
||||||
const value = {
|
const value = {
|
||||||
points,
|
points,
|
||||||
text1,
|
text1,
|
||||||
text2,
|
text2,
|
||||||
text3
|
text3,
|
||||||
};
|
};
|
||||||
arr.push(value);
|
arr.push(value);
|
||||||
startY += 180;
|
startY += 180;
|
||||||
}
|
}
|
||||||
return arr;
|
return arr;
|
||||||
});
|
});
|
||||||
|
|
||||||
const circleComputed = computed(() => {
|
const circleComputed = computed(() => {
|
||||||
const circles = [];
|
const circles = [];
|
||||||
let startY = config.startY;
|
let startY = config.startY;
|
||||||
@@ -53,7 +60,7 @@ const circleComputed = computed(() => {
|
|||||||
r: r.toFixed(1),
|
r: r.toFixed(1),
|
||||||
startX,
|
startX,
|
||||||
startY,
|
startY,
|
||||||
endX
|
endX,
|
||||||
};
|
};
|
||||||
circles.push(circle);
|
circles.push(circle);
|
||||||
endX = 1820;
|
endX = 1820;
|
||||||
|
|||||||
@@ -1,24 +1,33 @@
|
|||||||
<script 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();
|
||||||
|
|
||||||
const form = reactive({
|
const form = reactive({
|
||||||
username: "",
|
username: "",
|
||||||
password: ""
|
password: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const errorMessage = ref("");
|
const errorMessage = ref("");
|
||||||
|
|
||||||
const redirectPath = computed(() => {
|
const redirectPath = computed(() => {
|
||||||
const redirect = route.query.redirect;
|
const redirect = route.query.redirect;
|
||||||
if (typeof redirect === "string" && redirect.startsWith("/") && !redirect.startsWith("//")) {
|
if (
|
||||||
|
typeof redirect === "string" &&
|
||||||
|
redirect.startsWith("/") &&
|
||||||
|
!redirect.startsWith("//")
|
||||||
|
) {
|
||||||
return redirect;
|
return redirect;
|
||||||
}
|
}
|
||||||
return "/dashboard/integrated-center";
|
return "/dashboard/integrated-center";
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 校验登录表单,避免空账号或空密码提交到后端。
|
||||||
function validateLoginForm() {
|
function validateLoginForm() {
|
||||||
if (!form.username.trim()) {
|
if (!form.username.trim()) {
|
||||||
errorMessage.value = "请输入账号";
|
errorMessage.value = "请输入账号";
|
||||||
@@ -30,18 +39,23 @@ function validateLoginForm() {
|
|||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 提交登录信息,成功后回到登录前访问的页面。
|
||||||
async function handleLogin() {
|
async function handleLogin() {
|
||||||
if (!validateLoginForm() || loading.value) {
|
if (!validateLoginForm() || loading.value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
errorMessage.value = "";
|
errorMessage.value = "";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const encryptedForm = await encryptPasswordPayload(form, ["password"]);
|
const encryptedForm = await encryptPasswordPayload(form, ['password'])
|
||||||
await userStore.login(encryptedForm);
|
await userStore.login(encryptedForm)
|
||||||
router.replace(redirectPath.value);
|
router.replace(redirectPath.value);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
errorMessage.value = error instanceof Error ? error.message : "登录失败\,请检查账号或密码";
|
errorMessage.value =
|
||||||
|
error instanceof Error ? error.message : "登录失败,请检查账号或密码";
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
}
|
}
|
||||||
|
|||||||
8
src/vite-env.d.ts
vendored
Normal file
8
src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
|
/// <reference types="vue/jsx" />
|
||||||
|
|
||||||
|
declare module "*.vue" {
|
||||||
|
import { DefineComponent } from "vue";
|
||||||
|
const component: DefineComponent<{}, {}, any>;
|
||||||
|
export default component;
|
||||||
|
}
|
||||||
20
tsconfig.json
Normal file
20
tsconfig.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ESNext",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"strict": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"lib": ["ESNext", "DOM"],
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue", "src/api/rosCar.ts"]
|
||||||
|
}
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
import vue from "@vitejs/plugin-vue";
|
|
||||||
import vueJsx from "@vitejs/plugin-vue-jsx";
|
|
||||||
import { resolve } from "path";
|
|
||||||
export default ({ command }) => {
|
|
||||||
const isBuild = command === "build";
|
|
||||||
return {
|
|
||||||
plugins: [
|
|
||||||
vue(),
|
|
||||||
vueJsx()
|
|
||||||
],
|
|
||||||
define: {
|
|
||||||
__VUE_OPTIONS_API__: false
|
|
||||||
// 关闭 Vue2 中的 options选项API
|
|
||||||
},
|
|
||||||
base: "./",
|
|
||||||
// 使用相对路径
|
|
||||||
resolve: {
|
|
||||||
alias: [{ find: "@", replacement: resolve(__dirname, "src") }]
|
|
||||||
},
|
|
||||||
server: {
|
|
||||||
port: 6003,
|
|
||||||
host: true,
|
|
||||||
proxy: {
|
|
||||||
"/api": {
|
|
||||||
target: `http://192.168.3.110:16003/`,
|
|
||||||
changeOrigin: true,
|
|
||||||
rewrite: (path) => path.replace(/^\/api/, "")
|
|
||||||
},
|
|
||||||
"/ws": {
|
|
||||||
target: `ws://192.168.3.110:16003/`,
|
|
||||||
changeOrigin: true,
|
|
||||||
ws: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
oxc: {
|
|
||||||
//清除全局的console.log和debug
|
|
||||||
drop: isBuild ? ["console", "debugger"] : []
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
59
vite.config.ts
Normal file
59
vite.config.ts
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
import vueJsx from '@vitejs/plugin-vue-jsx'
|
||||||
|
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'
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig(({ command }: ConfigEnv) => {
|
||||||
|
const isBuild = command === 'build'
|
||||||
|
return ({
|
||||||
|
plugins: [
|
||||||
|
vue(),
|
||||||
|
vueJsx(),
|
||||||
|
Components({
|
||||||
|
resolvers: [NaiveUiResolver()]
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
define: {
|
||||||
|
__VUE_OPTIONS_API__: false // 关闭 Vue2 中的 options选项API
|
||||||
|
},
|
||||||
|
base: './', // 使用相对路径
|
||||||
|
resolve: {
|
||||||
|
alias: [{ find: '@', replacement: resolve(__dirname, 'src') }]
|
||||||
|
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
port: 6003,
|
||||||
|
host: true,
|
||||||
|
proxy: {
|
||||||
|
'/api': {
|
||||||
|
target: `http://127.0.0.1:16003/`,
|
||||||
|
changeOrigin: true,
|
||||||
|
rewrite: (path: string) => path.replace(/^\/api/, '')
|
||||||
|
},
|
||||||
|
'/ws': {
|
||||||
|
target: `ws://192.168.3.110:16003/`,
|
||||||
|
changeOrigin: true,
|
||||||
|
ws: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// Vite 8 原生方式:使用 Oxc Minifier 的 drop 选项
|
||||||
|
build: {
|
||||||
|
rolldownOptions: {
|
||||||
|
output: {
|
||||||
|
minify: {
|
||||||
|
compress: {
|
||||||
|
dropConsole: isBuild,
|
||||||
|
dropDebugger: isBuild,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user