Compare commits

2 Commits

Author SHA1 Message Date
cb8f2ae26e 将背景图像移动到::after伪元素中,避免z-index冲突问题 2026-07-02 19:00:28 +08:00
1db8bac454 重构为js 2026-07-02 18:42:32 +08:00
45 changed files with 1252 additions and 3093 deletions

View File

@@ -1,3 +1,3 @@
{
"recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"]
"recommendations": ["Vue.volar"]
}

View File

@@ -8,6 +8,6 @@
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
<script type="module" src="/src/main.js"></script>
</body>
</html>

177
package-lock.json generated
View File

@@ -24,9 +24,7 @@
"@vitejs/plugin-vue": "^4.1.0",
"@vitejs/plugin-vue-jsx": "^1.3.3",
"less": "^4.1.3",
"typescript": "^4.9.3",
"vite": "^4.2.0",
"vue-tsc": "^1.2.0"
"vite": "^4.2.0"
}
},
"node_modules/@babel/code-frame": {
@@ -1005,37 +1003,6 @@
"node": ">=12.0.0"
}
},
"node_modules/@volar/language-core": {
"version": "1.11.1",
"resolved": "https://registry.npmmirror.com/@volar/language-core/-/language-core-1.11.1.tgz",
"integrity": "sha512-dOcNn3i9GgZAcJt43wuaEykSluAuOkQgzni1cuxLxTV0nJKanQztp7FxyswdRILaKH+P2XZMPRp2S4MV/pElCw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@volar/source-map": "1.11.1"
}
},
"node_modules/@volar/source-map": {
"version": "1.11.1",
"resolved": "https://registry.npmmirror.com/@volar/source-map/-/source-map-1.11.1.tgz",
"integrity": "sha512-hJnOnwZ4+WT5iupLRnuzbULZ42L7BWWPMmruzwtLhJfpDVoZLjNBxHDi2sY2bgZXCKlpU5XcsMFoYrsQmPhfZg==",
"dev": true,
"license": "MIT",
"dependencies": {
"muggle-string": "^0.3.1"
}
},
"node_modules/@volar/typescript": {
"version": "1.11.1",
"resolved": "https://registry.npmmirror.com/@volar/typescript/-/typescript-1.11.1.tgz",
"integrity": "sha512-iU+t2mas/4lYierSnoFOeRFQUhAEMgsFuQxoxvwn5EdQopw43j+J27a4lt9LMInx1gLJBC6qL14WYGlgymaSMQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@volar/language-core": "1.11.1",
"path-browserify": "^1.0.1"
}
},
"node_modules/@vue/babel-helper-vue-transform-on": {
"version": "1.5.0",
"resolved": "https://registry.npmmirror.com/@vue/babel-helper-vue-transform-on/-/babel-helper-vue-transform-on-1.5.0.tgz",
@@ -1172,32 +1139,6 @@
"rfdc": "^1.4.1"
}
},
"node_modules/@vue/language-core": {
"version": "1.8.27",
"resolved": "https://registry.npmmirror.com/@vue/language-core/-/language-core-1.8.27.tgz",
"integrity": "sha512-L8Kc27VdQserNaCUNiSFdDl9LWT24ly8Hpwf1ECy3aFb9m6bDhBGQYOujDm21N7EW3moKIOKEanQwe1q5BK+mA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@volar/language-core": "~1.11.1",
"@volar/source-map": "~1.11.1",
"@vue/compiler-dom": "^3.3.0",
"@vue/shared": "^3.3.0",
"computeds": "^0.0.1",
"minimatch": "^9.0.3",
"muggle-string": "^0.3.1",
"path-browserify": "^1.0.1",
"vue-template-compiler": "^2.7.14"
},
"peerDependencies": {
"typescript": "*"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/@vue/reactivity": {
"version": "3.5.38",
"resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.5.38.tgz",
@@ -1284,13 +1225,6 @@
"proxy-from-env": "^2.1.0"
}
},
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true,
"license": "MIT"
},
"node_modules/baseline-browser-mapping": {
"version": "2.10.38",
"resolved": "https://registry.npmmirror.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.38.tgz",
@@ -1313,16 +1247,6 @@
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/brace-expansion": {
"version": "2.1.1",
"resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.1.1.tgz",
"integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
}
},
"node_modules/browserslist": {
"version": "4.28.4",
"resolved": "https://registry.npmmirror.com/browserslist/-/browserslist-4.28.4.tgz",
@@ -1403,13 +1327,6 @@
"node": ">= 0.8"
}
},
"node_modules/computeds": {
"version": "0.0.1",
"resolved": "https://registry.npmmirror.com/computeds/-/computeds-0.0.1.tgz",
"integrity": "sha512-7CEBgcMjVmitjYo5q8JTJVra6X5mQ20uTThdK+0kR7UEaDrAWEQcRiBtWJzga4eRpP6afNwwLsX2SET2JhVB1Q==",
"dev": true,
"license": "MIT"
},
"node_modules/convert-source-map": {
"version": "2.0.0",
"resolved": "https://registry.npmmirror.com/convert-source-map/-/convert-source-map-2.0.0.tgz",
@@ -1480,13 +1397,6 @@
"date-fns": "^3.0.0 || ^4.0.0"
}
},
"node_modules/de-indent": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/de-indent/-/de-indent-1.0.2.tgz",
"integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==",
"dev": true,
"license": "MIT"
},
"node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz",
@@ -1882,16 +1792,6 @@
"node": ">= 0.4"
}
},
"node_modules/he": {
"version": "1.2.0",
"resolved": "https://registry.npmmirror.com/he/-/he-1.2.0.tgz",
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
"dev": true,
"license": "MIT",
"bin": {
"he": "bin/he"
}
},
"node_modules/highlight.js": {
"version": "11.11.1",
"resolved": "https://registry.npmmirror.com/highlight.js/-/highlight.js-11.11.1.tgz",
@@ -2153,22 +2053,6 @@
"node": ">= 0.6"
}
},
"node_modules/minimatch": {
"version": "9.0.9",
"resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-9.0.9.tgz",
"integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.2"
},
"engines": {
"node": ">=16 || 14 >=14.17"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/minimist": {
"version": "1.2.8",
"resolved": "https://registry.npmmirror.com/minimist/-/minimist-1.2.8.tgz",
@@ -2190,13 +2074,6 @@
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/muggle-string": {
"version": "0.3.1",
"resolved": "https://registry.npmmirror.com/muggle-string/-/muggle-string-0.3.1.tgz",
"integrity": "sha512-ckmWDJjphvd/FvZawgygcUeQCxzvohjFO5RxTjj4eq8kw359gFF3E1brjfI+viLMxss5JrHTDRHZvu2/tuy0Qg==",
"dev": true,
"license": "MIT"
},
"node_modules/naive-ui": {
"version": "2.44.1",
"resolved": "https://registry.npmmirror.com/naive-ui/-/naive-ui-2.44.1.tgz",
@@ -2335,13 +2212,6 @@
"node": ">= 0.10"
}
},
"node_modules/path-browserify": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/path-browserify/-/path-browserify-1.0.1.tgz",
"integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==",
"dev": true,
"license": "MIT"
},
"node_modules/pbf": {
"version": "3.2.1",
"resolved": "https://registry.npmmirror.com/pbf/-/pbf-3.2.1.tgz",
@@ -2679,8 +2549,9 @@
"version": "4.9.5",
"resolved": "https://registry.npmmirror.com/typescript/-/typescript-4.9.5.tgz",
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
"devOptional": true,
"license": "Apache-2.0",
"optional": true,
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -2848,48 +2719,6 @@
"integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==",
"license": "MIT"
},
"node_modules/vue-template-compiler": {
"version": "2.7.16",
"resolved": "https://registry.npmmirror.com/vue-template-compiler/-/vue-template-compiler-2.7.16.tgz",
"integrity": "sha512-AYbUWAJHLGGQM7+cNTELw+KsOG9nl2CnSv467WobS5Cv9uk3wFcnr1Etsz2sEIHEZvw1U+o9mRlEO6QbZvUPGQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"de-indent": "^1.0.2",
"he": "^1.2.0"
}
},
"node_modules/vue-tsc": {
"version": "1.8.27",
"resolved": "https://registry.npmmirror.com/vue-tsc/-/vue-tsc-1.8.27.tgz",
"integrity": "sha512-WesKCAZCRAbmmhuGl3+VrdWItEvfoFIPXOvUJkjULi+x+6G/Dy69yO3TBRJDr9eUlmsNAwVmxsNZxvHKzbkKdg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@volar/typescript": "~1.11.1",
"@vue/language-core": "1.8.27",
"semver": "^7.5.4"
},
"bin": {
"vue-tsc": "bin/vue-tsc.js"
},
"peerDependencies": {
"typescript": "*"
}
},
"node_modules/vue-tsc/node_modules/semver": {
"version": "7.8.5",
"resolved": "https://registry.npmmirror.com/semver/-/semver-7.8.5.tgz",
"integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/vueuc": {
"version": "0.4.65",
"resolved": "https://registry.npmmirror.com/vueuc/-/vueuc-0.4.65.tgz",

View File

@@ -5,7 +5,7 @@
"type": "module",
"scripts": {
"dev": "vite",
"build": "vue-tsc && vite build",
"build": "vite build",
"preview": "vite preview",
"format": "prettier --write ."
},
@@ -26,8 +26,6 @@
"@vitejs/plugin-vue": "^4.1.0",
"@vitejs/plugin-vue-jsx": "^1.3.3",
"less": "^4.1.3",
"typescript": "^4.9.3",
"vite": "^4.2.0",
"vue-tsc": "^1.2.0"
"vite": "^4.2.0"
}
}

View File

@@ -1,40 +1,32 @@
<script lang="ts" setup>
<script setup>
import { computed, onBeforeUnmount, onMounted, ref } from "vue";
const DESIGN_WIDTH = 1920;
const DESIGN_HEIGHT = 1080;
const viewportWidth = ref(0);
const viewportHeight = ref(0);
const scale = ref(1);
function updateScreenAdapter() {
viewportWidth.value = window.innerWidth;
viewportHeight.value = window.innerHeight;
const widthScale = viewportWidth.value / DESIGN_WIDTH;
const heightScale = viewportHeight.value / DESIGN_HEIGHT;
scale.value = Math.min(widthScale, heightScale);
}
const adapterStyle = computed(() => {
const scaledWidth = DESIGN_WIDTH * scale.value;
const scaledHeight = DESIGN_HEIGHT * scale.value;
return {
width: `${DESIGN_WIDTH}px`,
height: `${DESIGN_HEIGHT}px`,
left: `${(viewportWidth.value - scaledWidth) / 2}px`,
top: `${(viewportHeight.value - scaledHeight) / 2}px`,
transform: `scale(${scale.value})`,
transform: `scale(${scale.value})`
};
});
onMounted(() => {
updateScreenAdapter();
window.addEventListener("resize", updateScreenAdapter);
});
onBeforeUnmount(() => {
window.removeEventListener("resize", updateScreenAdapter);
});

18
src/api/alarm.js Normal file
View File

@@ -0,0 +1,18 @@
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
};

View File

@@ -1,76 +0,0 @@
import request from "@/utils/request";
export interface RosInspectionRecordLineImageQueryDTO {
recognizeType?: number | string;
startTime?: string;
endTime?: string;
warning?: boolean;
keyword?: string;
}
export interface OrderItem {
property: string;
tableIndex: number;
asc?: boolean;
}
export interface PageParam<T> {
page?: number;
limit?: number;
orderList?: OrderItem[];
query?: T;
}
export type RosInspectionRecordLineImagePageQuery =
Partial<RosInspectionRecordLineImage> & {
keyword?: string;
};
export interface RosInspectionRecordLineImageCar {
id?: string;
deviceKey?: string;
deviceNo?: string;
name?: string;
}
export interface RosInspectionRecordLineImage {
recordId: string;
carId?: string;
car?: RosInspectionRecordLineImageCar;
recordActionId: string;
taskId?: string;
recognizeType?: number;
imageUrl?: string;
captureTime?: string;
poseX?: number;
poseY?: number;
poseYaw?: number;
recognitionStatus?: number;
prompt?: string;
recognitionResult?: string;
warning?: boolean;
warningValue?: string;
failReason?: string;
startTime?: string;
endTime?: string;
createTime?: string;
updateTime?: string;
id?: string;
}
// 查询近期告警列表
export function getRecentAlarmList(data: RosInspectionRecordLineImageQueryDTO) {
return request({
url: "/admin/rosInspectionRecordLineImage/warnList",
data,
});
}
// 分页查询告警列表
export function getAlarmPage(data: PageParam<RosInspectionRecordLineImagePageQuery>) {
return request({
url: "/admin/rosInspectionRecordLineImage/list",
method: "POST",
data,
});
}

61
src/api/rosCar.js Normal file
View File

@@ -0,0 +1,61 @@
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
};

View File

@@ -1,73 +0,0 @@
import request from "@/utils/request";
// 小车分页列表
export function getRosCarList(data: unknown) {
return request({
url: "/admin/rosCar/list",
data,
});
}
// 保存巡检任务模板
export function saveRosInspectionTask(data: unknown) {
return request({
url: "/admin/rosInspectionTask/save",
data,
});
}
// 巡检任务模板详情
export function getRosInspectionTaskDetail(cardId: string) {
return request({
url: "/admin/rosInspectionTask/detail",
params: { cardId },
});
}
// 查询小车当前巡检状态
export function getCurrentRosInspection(carId: string) {
return request({
url: "/admin/rosCar/currentInspection",
params: { carId },
});
}
// 下发巡检任务
export function dispatchRosInspectionTask(taskId: string) {
return request({
url: "/admin/rosInspectionTask/dispatch",
params: { taskId },
});
}
// 停止已下发的巡检任务
export function stopDispatchRosInspectionTask(taskId: string) {
return request({
url: "/admin/rosInspectionTask/stopDispatch",
params: { taskId },
});
}
// 上传文件并返回后端保存后的访问地址
export function uploadFile(file: File) {
const data = new FormData();
data.append("file", file);
return request({
url: "/admin/file/upload",
data,
});
}
// 更新小车地图文件地址
export function updateRosCarMapUrls(data: {
id: string;
mapPgmUrl?: string;
mapYamlUrl?: string;
}) {
return request({
url: "/admin/rosCar/updateMapUrls",
data,
});
}

View File

@@ -0,0 +1,8 @@
@font-face {
/*给字体命名*/
font-family: 'YouSheBiaoTiHei';
/*引入字体文件*/
src: url('./YouSheBiaoTiHei.ttf');
font-weight: normal;
font-style: normal;
}

Binary file not shown.

View File

@@ -1,42 +1,45 @@
<script lang="ts" setup>
import { onBeforeUnmount, onMounted, ref, watch } from "vue";
<script setup>
import { nextTick, onBeforeUnmount, onMounted, ref, watch } from "vue";
import * as echarts from "echarts";
import { EChartsOption } from "echarts";
let chart: echarts.ECharts | null = null;
const chartRef = ref<HTMLElement | undefined>();
let chart = null;
let resizeObserver = null;
const chartRef = ref();
const props = defineProps({
option: {
type: Object as () => EChartsOption | {},
required: true,
},
type: Object,
required: true
}
});
function applyOption(option?: EChartsOption | Record<string, never>) {
function applyOption(option) {
if (!chart || !option) {
return;
}
chart.clear();
chart.setOption(option);
}
onMounted(() => {
chart = echarts.init(chartRef.value, "t-theme");
applyOption(props.option);
resizeObserver = new ResizeObserver(() => {
chart?.resize();
});
resizeObserver.observe(chartRef.value);
nextTick(() => {
chart?.resize();
});
});
watch(
() => props.option,
(value) => {
applyOption(value);
},
{ deep: true },
{ deep: true }
);
onBeforeUnmount(() => {
if (resizeObserver) {
resizeObserver.disconnect();
resizeObserver = null;
}
if (chart) {
chart.dispose();
chart = null;

View File

@@ -8,8 +8,8 @@ const color = [
colorStops: [
{ offset: 0, color: "#94c7fd" },
{ offset: 0.5, color: "#3b99fb" },
{ offset: 1, color: "#004ca0" },
],
{ offset: 1, color: "#004ca0" }
]
},
{
type: "linear",
@@ -20,8 +20,8 @@ const color = [
colorStops: [
{ offset: 0, color: "#b070c0" },
{ offset: 0.5, color: "#b070c0" },
{ offset: 1, color: "#5d235d" },
],
{ offset: 1, color: "#5d235d" }
]
},
{
type: "linear",
@@ -32,8 +32,8 @@ const color = [
colorStops: [
{ offset: 0, color: "#7eedd6" },
{ offset: 0.5, color: "#3be8bb" },
{ offset: 1, color: "#00b38a" },
],
{ offset: 1, color: "#00b38a" }
]
},
{
type: "linear",
@@ -44,8 +44,8 @@ const color = [
colorStops: [
{ offset: 0, color: "#fff1f0" },
{ offset: 0.5, color: "#ff9494" },
{ offset: 1, color: "#ff4d4f" },
],
{ offset: 1, color: "#ff4d4f" }
]
},
{
type: "linear",
@@ -56,8 +56,8 @@ const color = [
colorStops: [
{ offset: 0, color: "#fffbe6" },
{ offset: 0.5, color: "#ffe58f" },
{ offset: 1, color: "#ffbb96" },
],
{ offset: 1, color: "#ffbb96" }
]
},
{
type: "linear",
@@ -68,8 +68,8 @@ const color = [
colorStops: [
{ offset: 0, color: "#ff3333" },
{ offset: 0.5, color: "#ff0000" },
{ offset: 1, color: "#cc0000" },
],
{ offset: 1, color: "#cc0000" }
]
},
{
type: "linear",
@@ -80,25 +80,24 @@ const color = [
colorStops: [
{ offset: 0, color: "#ffe6f0" },
{ offset: 0.5, color: "#ffb3d9" },
{ offset: 1, color: "#ff85c0" },
],
},
{ offset: 1, color: "#ff85c0" }
]
}
];
export default {
color,
textStyle: {
fontSize: 12,
color: "#fff",
color: "#fff"
},
legend: {
textStyle: {
color: "#fff",
},
color: "#fff"
}
},
tooltip: {
textStyle: {
align: "left",
},
},
align: "left"
}
}
};

View File

@@ -1,35 +1,30 @@
import { Map } from "ol";
import VectorSource from "ol/source/Vector";
import { GeoJSON } from "ol/format";
import VectorLayer from "ol/layer/Vector";
import { Fill, Stroke, Style } from "ol/style";
import { FeatureLike } from "ol/Feature";
import { MultiPolygon } from "ol/geom";
export default function (mapInstance: Map) {
export default function (mapInstance) {
const vectorSource = new VectorSource({
url: "https://geo.datav.aliyun.com/areas_v3/bound/330000.json",
format: new GeoJSON(),
format: new GeoJSON()
});
for (let i = 1; i <= 3; i++) {
const layer = new VectorLayer({
zIndex: -1,
source: vectorSource,
style: (feature: FeatureLike) => {
const geometry = (feature.getGeometry() as MultiPolygon).clone();
style: (feature) => {
const geometry = feature.getGeometry().clone();
geometry.translate(0, -0.04 * i);
return new Style({
fill: new Fill({
color: "rgba(176, 166, 132,0.6)",
color: "rgba(176, 166, 132,0.6)"
}),
stroke: new Stroke({
color: "rgba(176, 166, 132,0.6)",
width: 1,
width: 1
}),
geometry,
geometry
});
},
}
});
mapInstance.addLayer(layer);
}

View File

@@ -7,33 +7,28 @@ import { Fill, Stroke, Style, Text } from "ol/style";
import { getBottomLeft, getCenter, getHeight, getWidth } from "ol/extent";
import materialJpg from "@/assets/map-material.jpg";
import { toContext } from "ol/render";
import { MultiPolygon } from "ol/geom";
import { defaults } from "ol/interaction";
export default function useMap(target: string) {
function useMap(target) {
const mapInstance = new Map({
view: new View({
projection: "EPSG:4326",
zoom: 12,
center: [0, 0],
center: [0, 0]
}),
controls: [],
interactions: defaults({
dragPan: false,
mouseWheelZoom: false,
}),
mouseWheelZoom: false
})
});
const vectorSource = new VectorSource({
url: "https://geo.datav.aliyun.com/areas_v3/bound/330000_full.json",
format: new GeoJSON(),
format: new GeoJSON()
});
const customStyle = new Style({
renderer: function(pixelCoordinates, state) {
const context = state.context;
const multiPolygon = state.geometry.clone() as MultiPolygon;
// @ts-ignore
const multiPolygon = state.geometry.clone();
multiPolygon.setCoordinates(pixelCoordinates);
const extent = multiPolygon.getExtent();
const width = getWidth(extent);
@@ -44,14 +39,14 @@ export default function useMap(target: string) {
}
context.save();
const renderContext = toContext(context, {
pixelRatio: 1,
pixelRatio: 1
});
renderContext.setFillStrokeStyle(
new Fill(),
new Stroke({
color: "rgba(255,255,255,0.8)",
width: 3,
}),
width: 3
})
);
renderContext.drawMultiPolygon(multiPolygon);
context.clip();
@@ -60,9 +55,8 @@ export default function useMap(target: string) {
const bottom = bottomLeft[1];
context.drawImage(flag, left, bottom, width, height);
context.restore();
},
}
});
const layer = new VectorLayer({
source: vectorSource,
style: (feature) => {
@@ -72,16 +66,15 @@ export default function useMap(target: string) {
text: new Text({
text: feature.get("name"),
fill: new Fill({
color: "#fff",
color: "#fff"
}),
scale: 1.5,
}),
zIndex: 1,
scale: 1.5
}),
zIndex: 1
})
];
},
}
});
vectorSource.on("addfeature", function(event) {
const feature = event.feature;
const img = new Image();
@@ -90,22 +83,18 @@ export default function useMap(target: string) {
feature?.set("material", img);
};
});
vectorSource.on("featuresloadend", (event) => {
const view = mapInstance.getView();
const extent = event.target.getExtent();
view.fit(extent, { padding: [0, 30, 0, 0] });
view.setCenter(getCenter(extent));
});
mapInstance.addLayer(layer);
onMounted(() => {
mapInstance.setTarget(target);
});
return {
mapInstance,
mapInstance
};
}
export default useMap;

View File

@@ -4,20 +4,17 @@ import App from "./App.vue";
import Chart from "./components/chart/index.vue";
import * as echarts from "echarts";
import echartsConfig from "./config/echarts.config";
import { createDiscreteApi } from "naive-ui";
import naive, { createDiscreteApi } from "naive-ui";
import { createPinia } from "pinia";
import router from "./router";
import '../src/assets/font/YouSheBiaoTiHei.css';
echarts.registerTheme("t-theme", echartsConfig);
const { message, dialog } = createDiscreteApi(["message", "dialog"]);
window.$message = message;
window.$modal = dialog;
const app = createApp(App);
app.use(createPinia());
app.use(router);
app.use(naive);
app.component("TChart", Chart);
app.mount("#app");

View File

@@ -1,16 +1,13 @@
import { getToken } from "@/utils/auth";
import {
createRouter,
createWebHashHistory,
type RouteRecordRaw,
createWebHashHistory
} from "vue-router";
const dashboardDefaultPath = "/dashboard/integrated-center";
const routes: RouteRecordRaw[] = [
const routes = [
{
path: "/",
redirect: () => (getToken() ? dashboardDefaultPath : "/login"),
redirect: () => getToken() ? dashboardDefaultPath : "/login"
},
{
path: "/dashboard",
@@ -20,76 +17,67 @@ const routes: RouteRecordRaw[] = [
{
path: "integrated-center",
name: "IntegratedCenter",
component: () =>
import("@/views/dashboard1/pages/IntegratedCenterPage.vue"),
meta: { title: "监管中心", tabId: 1, },
component: () => import("@/views/dashboard1/pages/IntegratedCenterPage.vue"),
meta: { title: "监管中心", tabId: 1 }
},
{
path: "equipment-management",
name: "EquipmentManagement",
component: () =>
import("@/views/dashboard1/pages/EquipmentManagementPage.vue"),
meta: { title: "设备管理", tabId: 2, },
component: () => import("@/views/dashboard1/pages/EquipmentManagementPage.vue"),
meta: { title: "设备管理", tabId: 2 }
},
{
path: "video-center",
name: "VideoCenter",
component: () => import("@/views/dashboard1/pages/VideoCenterPage.vue"),
meta: { title: "视频中心", tabId: 3, },
meta: { title: "视频中心", tabId: 3 }
},
{
path: "alarm-management",
name: "AlarmManagement",
component: () =>
import("@/views/dashboard1/pages/AlarmManagementPage.vue"),
meta: { title: "告警管理", tabId: 4, },
component: () => import("@/views/dashboard1/pages/AlarmManagementPage.vue"),
meta: { title: "告警管理", tabId: 4 }
},
{
path: "patrol-plan",
name: "PatrolPlan",
component: () => import("@/views/dashboard1/pages/PatrolPlanPage.vue"),
meta: { title: "巡检记录", tabId: 5, },
meta: { title: "巡检记录", tabId: 5 }
},
{
path: "data-report",
name: "DataReport",
component: () => import("@/views/dashboard1/pages/DataReportPage.vue"),
meta: { title: "统计分析", tabId: 6, },
},
],
meta: { title: "统计分析", tabId: 6 }
}
]
},
{
path: "/login",
name: "Login",
component: () => import("@/views/login/index.vue"),
meta: { title: "登录" },
meta: { title: "登录" }
},
{
path: "/:pathMatch(.*)*",
redirect: dashboardDefaultPath,
},
redirect: dashboardDefaultPath
}
];
const router = createRouter({
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 },
query: { redirect: to.fullPath }
};
}
return true;
});
export function resetRouter() {
// 当前项目暂无动态 addRoute 路由,保留该方法兼容登出流程。
}

View File

@@ -1,63 +0,0 @@
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;
};
}
declare module "@/utils/auth" {
export function getToken(): string | null;
export function getTokenName(): string | null;
export function setToken(tokenValue: string, tokenNameValue?: string): void;
export function removeToken(): void;
}
declare module "@/utils/auth.js" {
export function getToken(): string | null;
export function getTokenName(): string | null;
export function setToken(tokenValue: string, tokenNameValue?: string): void;
export function removeToken(): void;
}
declare module "@/store/modules/user" {
export function useUserStore(): {
login(userInfo: Record<string, unknown>): Promise<boolean>;
logout(): Promise<boolean>;
};
}
declare module "@/store/modules/user.js" {
export function useUserStore(): {
login(userInfo: Record<string, unknown>): Promise<boolean>;
logout(): Promise<boolean>;
};
}
declare module "@/utils/passwordEncrypt.js" {
export function encryptPasswordPayload<T extends Record<string, unknown>>(
payload: T,
fields: Array<keyof T>,
): Promise<T>;
}
declare module "@/utils/request" {
import type { AxiosRequestConfig } from "axios";
export const BASE_URL: string;
const request: <T = unknown>(config: AxiosRequestConfig) => Promise<T>;
export default request;
}
declare module "@/utils/request.js" {
import type { AxiosRequestConfig } from "axios";
export const BASE_URL: string;
const request: <T = unknown>(config: AxiosRequestConfig) => Promise<T>;
export default request;
}

View File

@@ -1,10 +1,4 @@
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[] {
function generateNumbers(min, max, count) {
if (!count) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
@@ -15,5 +9,6 @@ function generateNumbers(
}
return numbers;
}
export { generateNumbers };
export {
generateNumbers
};

View File

@@ -0,0 +1,24 @@
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>;
}
});

View File

@@ -1,29 +0,0 @@
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>
);
},
});

View File

@@ -1,51 +1,44 @@
import { onBeforeUnmount, ref } from "vue";
import { EChartsOption } from "echarts";
import { generateNumbers } from "@/utils";
export default function () {
const option = ref<EChartsOption>({});
const option = ref({});
function refresh() {
option.value = {
xAxis: {
type: "category",
data: ["周一", "周二", "周三", "周四", "周五", "周六", "周日"],
data: ["周一", "周二", "周三", "周四", "周五", "周六", "周日"]
},
yAxis: {
type: "value",
type: "value"
},
grid: {
left: "12%",
left: "12%"
},
series: [
{
data: generateNumbers(1, 300, 7),
type: "line",
type: "line"
},
{
data: generateNumbers(1, 300, 7),
type: "line",
type: "line"
},
{
data: generateNumbers(1, 300, 7),
type: "line",
},
],
type: "line"
}
]
};
}
refresh();
const timer = setInterval(() => {
refresh();
}, 3000);
}, 3e3);
onBeforeUnmount(() => {
clearInterval(timer);
});
return {
option,
refresh,
refresh
};
}

View File

@@ -1,18 +1,15 @@
import { onBeforeUnmount, ref } from "vue";
import { EChartsOption } from "echarts";
import { generateNumbers } from "@/utils";
export default function () {
const option = ref<EChartsOption>({});
const option = ref({});
function refresh() {
option.value = {
tooltip: {
trigger: "item",
trigger: "item"
},
legend: {
top: "5%",
left: "center",
left: "center"
},
series: [
{
@@ -21,46 +18,42 @@ export default function () {
radius: ["40%", "70%"],
avoidLabelOverlap: false,
itemStyle: {
borderColor: "#fff",
borderColor: "#fff"
},
label: {
show: false,
position: "center",
position: "center"
},
emphasis: {
label: {
show: true,
fontSize: 30,
fontWeight: "bold",
},
fontWeight: "bold"
}
},
labelLine: {
show: false,
show: false
},
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: "产品五" }
]
}
]
};
}
refresh();
const timer = setInterval(() => {
refresh();
}, 3000);
}, 3e3);
onBeforeUnmount(() => {
clearInterval(timer);
});
return {
option,
refresh,
refresh
};
}

View File

@@ -1,15 +1,12 @@
import { onBeforeUnmount, ref } from "vue";
import { EChartsOption } from "echarts";
import { generateNumbers } from "@/utils";
export default function () {
const option = ref<EChartsOption>({});
const option = ref({});
function refresh() {
option.value = {
tooltip: {
trigger: "item",
formatter: "{a} <br/>{b} : {c}%",
formatter: "{a} <br/>{b} : {c}%"
},
series: [
{
@@ -18,57 +15,53 @@ export default function () {
left: "center",
width: "100%",
min: 0,
max: 1000,
max: 1e3,
minSize: "0%",
maxSize: "100%",
sort: "descending",
gap: 2,
label: {
show: true,
position: "inside",
position: "inside"
},
labelLine: {
length: 10,
lineStyle: {
width: 1,
type: "solid",
},
type: "solid"
}
},
itemStyle: {
borderColor: "#fff",
borderWidth: 1,
borderWidth: 1
},
emphasis: {
label: {
fontSize: 20,
},
fontSize: 20
}
},
data: [
{ value: generateNumbers(500, 1000), name: "访问量" },
{ value: generateNumbers(500, 1000), name: "注册量" },
{ value: generateNumbers(500, 1000), name: "浏览量" },
{ value: generateNumbers(500, 1e3), name: "访问量" },
{ value: generateNumbers(500, 1e3), name: "注册量" },
{ value: generateNumbers(500, 1e3), name: "浏览量" },
{ value: generateNumbers(200, 500), name: "加入购物车" },
{ value: generateNumbers(100, 800), name: "购买" },
{ value: generateNumbers(1, 20), name: "退货" },
{ value: generateNumbers(1, 20), name: "取消订单" },
],
},
],
{ value: generateNumbers(1, 20), name: "取消订单" }
]
}
]
};
}
refresh();
const timer = setInterval(() => {
refresh();
}, 3000);
}, 3e3);
onBeforeUnmount(() => {
clearInterval(timer);
});
return {
option,
refresh,
refresh
};
}

View File

@@ -1,10 +1,7 @@
import { onBeforeUnmount, ref } from "vue";
import { EChartsOption } from "echarts";
import { generateNumbers } from "@/utils";
export default function () {
const option = ref<EChartsOption>({});
const option = ref({});
function refresh() {
option.value = {
radar: {
@@ -14,9 +11,9 @@ export default function () {
{ name: "品质", max: 200 },
{ name: "客户支持", max: 200 },
{ name: "可持续性", max: 200 },
{ name: "效果", max: 200 },
{ name: "效果", max: 200 }
],
radius: 100,
radius: 100
},
series: [
{
@@ -24,26 +21,22 @@ export default function () {
data: [
{
value: generateNumbers(1, 200, 6),
areaStyle: {},
},
],
},
],
areaStyle: {}
}
]
}
]
};
}
refresh();
const timer = setInterval(() => {
refresh();
}, 3000);
}, 3e3);
onBeforeUnmount(() => {
clearInterval(timer);
});
return {
option,
refresh,
refresh
};
}

View File

@@ -1,43 +1,40 @@
import { onBeforeUnmount, ref } from "vue";
import { EChartsOption } from "echarts";
import { generateNumbers } from "@/utils";
export default function () {
const option = ref<EChartsOption>({});
const option = ref({});
function refresh() {
option.value = {
tooltip: {
trigger: "axis",
axisPointer: {
type: "shadow",
},
type: "shadow"
}
},
legend: {
top: "5%",
itemStyle: {
borderColor: "#fff",
borderWidth: 2,
},
borderWidth: 2
}
},
grid: {
left: "10%",
bottom: "8%",
containLabel: true,
containLabel: true
},
xAxis: [
{
type: "value",
},
type: "value"
}
],
yAxis: [
{
type: "category",
axisTick: {
show: false,
},
data: ["一月", "二月", "三月", "四月", "五月", "六月", "七月"],
show: false
},
data: ["一月", "二月", "三月", "四月", "五月", "六月", "七月"]
}
],
series: [
{
@@ -45,53 +42,49 @@ export default function () {
type: "bar",
label: {
show: true,
position: "inside",
position: "inside"
},
emphasis: {
focus: "series",
focus: "series"
},
data: generateNumbers(1, 200, 7),
data: generateNumbers(1, 200, 7)
},
{
name: "购买",
type: "bar",
stack: "Total",
label: {
show: true,
show: true
},
emphasis: {
focus: "series",
focus: "series"
},
data: generateNumbers(1, 300, 7),
data: generateNumbers(1, 300, 7)
},
{
name: "退货",
type: "bar",
stack: "Total",
label: {
show: true,
show: true
},
emphasis: {
focus: "series",
focus: "series"
},
data: generateNumbers(-50, -10, 7),
},
],
data: generateNumbers(-50, -10, 7)
}
]
};
}
refresh();
const timer = setInterval(() => {
refresh();
}, 3000);
}, 3e3);
onBeforeUnmount(() => {
clearInterval(timer);
});
return {
option,
refresh,
refresh
};
}

View File

@@ -1,10 +1,7 @@
import { onBeforeUnmount, ref } from "vue";
import { EChartsOption } from "echarts";
import { generateNumbers } from "@/utils";
export default function () {
const option = ref<EChartsOption>({});
const option = ref({});
function refresh() {
option.value = {
xAxis: {},
@@ -19,8 +16,8 @@ export default function () {
generateNumbers(1, 10, 2),
generateNumbers(1, 10, 2),
[8.07, 6.95],
[13.0, 7.58],
[14.0, 7.66],
[13, 7.58],
[14, 7.66],
[13.4, 6.81],
generateNumbers(1, 10, 2),
generateNumbers(1, 10, 2),
@@ -34,26 +31,22 @@ export default function () {
generateNumbers(1, 10, 2),
generateNumbers(1, 10, 2),
generateNumbers(1, 10, 2),
[5.02, 5.68],
],
type: "scatter",
},
[5.02, 5.68]
],
type: "scatter"
}
]
};
}
refresh();
const timer = setInterval(() => {
refresh();
}, 3000);
}, 3e3);
onBeforeUnmount(() => {
clearInterval(timer);
});
return {
option,
refresh,
refresh
};
}

View File

@@ -2,6 +2,15 @@
width: 1920px;
height: 1080px;
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"),
url("./assets/整体边框_静态图片.png"), url("./assets/底部动画.webp"),
url("./assets/底部动画.png"), url("./assets/头部动画_左侧点.webp"),
@@ -27,23 +36,33 @@
auto,
auto,
auto;
}
&:before {
content: "管廊巡检机器人综合管理平台";
color: #fff;
letter-spacing: 12px;
font-size: 36px;
line-height: 70px;
width: 100%;
font-weight: bold;
color: #FFFFFF;
background: linear-gradient(0deg, #02B3FE 0%, #F9FDFF 100%);
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
font-family: YouSheBiaoTiHei, system-ui;
font-size: 42px;
position: absolute;
top: 0;
left: 0;
right: 0;
z-index: 2;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
}
}
.background {
position: relative;
z-index: -1;
position: absolute;
inset: 0;
z-index: 0;
width: 100%;
height: 100%;
object-fit: cover;
@@ -51,6 +70,7 @@
.main {
position: absolute;
z-index: 2;
width: 100%;
height: 100%;
top: 0;
@@ -256,6 +276,27 @@
overflow: auto;
border: 1px solid rgba(77, 188, 248, 0.12);
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 {
@@ -267,7 +308,8 @@
.device-table th,
.device-table td {
padding: 14px 16px;
text-align: left;
text-align: center;
vertical-align: middle;
border-bottom: 1px solid rgba(77, 188, 248, 0.08);
color: #dff7ff;
font-size: 13px;
@@ -381,6 +423,7 @@
.table-actions {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
@@ -443,6 +486,31 @@
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 {
position: fixed;
inset: 0;
@@ -1644,6 +1712,17 @@
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 {
position: absolute;
z-index: 3;

View File

@@ -1,34 +1,24 @@
<script lang="ts" setup>
<script setup>
import { computed } from "vue";
import { useRoute, useRouter } from "vue-router";
import SwitchPageBtn from "./components/btn";
import SwitchPageBtn from "./components/btn/index.jsx";
import backgroundVideo from "./assets/循环背景动画.mp4";
interface DashboardTab {
id: number;
text: string;
path: string;
}
const route = useRoute();
const router = useRouter();
const tabs: Record<"left" | "right", DashboardTab[]> = {
const tabs = {
left: [
{ id: 1, text: "监管中心", path: "/dashboard/integrated-center" },
{ id: 2, text: "设备管理", path: "/dashboard/equipment-management" },
{ id: 3, text: "视频中心", path: "/dashboard/video-center" },
{ id: 3, text: "视频中心", path: "/dashboard/video-center" }
],
right: [
{ id: 4, text: "告警管理", path: "/dashboard/alarm-management" },
{ 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);
function switchPage(tab: DashboardTab) {
function switchPage(tab) {
if (route.path !== tab.path) {
router.push(tab.path);
}

View File

@@ -1,104 +1,65 @@
<script lang="ts" setup>
import { NImage } from "naive-ui";
<script setup>
import { computed, onMounted, ref } from "vue";
import {
getAlarmPage,
type RosInspectionRecordLineImage,
getAlarmPage
} from "@/api/alarm";
import { BASE_URL } from "@/utils/request";
import { getRecentAlertPhoto } from "../shared/alertPhotos";
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> = {
const recognizeTypeMap = {
100: "局部高温",
1: "电缆表面破损",
2: "桥架断裂",
3: "隧道积水",
4: "墙体裂缝",
4: "墙体裂缝"
};
const recognitionStatusMap: Record<number, RecognitionStatusText> = {
const recognitionStatusMap = {
0: "待执行",
1: "执行中",
2: "执行成功",
3: "执行失败",
3: "执行失败"
};
const alarmNo = ref("");
const recognizeType = ref("");
const warningFilter = ref<WarningFilterValue>("true");
const warningFilter = ref("true");
const startTime = ref("");
const endTime = ref("");
const alarms = ref<AlarmRecord[]>([]);
const alarms = ref([]);
const totalAlarms = ref(0);
const listLoading = ref(false);
const listError = ref("");
const pageSize = 10;
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 visiblePages = computed(() => {
const pages: number[] = [];
const pages = [];
const start = Math.max(1, currentPage.value - 2);
const end = Math.min(totalPages.value, start + 4);
for (let page = start; page <= end; page += 1) {
pages.push(page);
}
return pages;
});
function getAlarmStatusClass(status: AlarmRecord["status"]) {
const classMap: Record<AlarmRecord["status"], string> = {
function getAlarmStatusClass(status) {
const classMap = {
待执行: "alarm-status-pending",
执行中: "alarm-status-handling",
执行成功: "alarm-status-finished",
执行失败: "alarm-status-false",
未知状态: "alarm-status-false",
未知状态: "alarm-status-false"
};
return classMap[status];
}
function setPage(page: number) {
function setPage(page) {
currentPage.value = Math.min(Math.max(page, 1), totalPages.value);
fetchAlarmList();
}
function handleSearch() {
currentPage.value = 1;
fetchAlarmList();
}
function handleClear() {
alarmNo.value = "";
recognizeType.value = "";
@@ -107,14 +68,11 @@ function handleClear() {
endTime.value = "";
handleSearch();
}
// 请求告警分页列表并更新表格数据
async function fetchAlarmList() {
listLoading.value = true;
listError.value = "";
try {
const response = (await getAlarmPage({
const response = await getAlarmPage({
page: currentPage.value,
limit: pageSize,
query: {
@@ -122,12 +80,11 @@ async function fetchAlarmList() {
warning: normalizeWarningFilter(warningFilter.value),
startTime: normalizeDateTime(startTime.value),
endTime: normalizeDateTime(endTime.value),
id: alarmNo.value.trim() || undefined,
},
})) as AlarmPageResponse;
id: alarmNo.value.trim() || void 0
}
});
const result = response.data;
const rows = result?.data ?? [];
alarms.value = rows.map((record, index) => normalizeAlarmRecord(record, index));
totalAlarms.value = result?.total ?? rows.length;
} catch (error) {
@@ -139,11 +96,8 @@ async function fetchAlarmList() {
listLoading.value = false;
}
}
// 将后端告警记录转换为页面表格展示结构
function normalizeAlarmRecord(record: RosInspectionRecordLineImage, index: number): AlarmRecord {
function normalizeAlarmRecord(record, index) {
const type = getRecognizeTypeText(record.recognizeType);
return {
id: record.id || record.recordActionId || record.recordId || `AL-${index + 1}`,
type,
@@ -153,64 +107,45 @@ function normalizeAlarmRecord(record: RosInspectionRecordLineImage, index: numbe
time: record.captureTime || record.createTime || "--",
position: formatPosition(record),
result: record.warningValue || record.recognitionResult || record.failReason || "--",
warning: Boolean(record.warning),
warning: Boolean(record.warning)
};
}
function getCarName(record: RosInspectionRecordLineImage) {
function getCarName(record) {
return record.car?.name || record.car?.deviceNo || record.car?.deviceKey || record.carId || "未知设备";
}
function getRecognizeTypeText(type?: number) {
function getRecognizeTypeText(type) {
return type ? recognizeTypeMap[type] ?? "未知告警" : "未知告警";
}
function getRecognitionStatusText(status?: number) {
function getRecognitionStatusText(status) {
return typeof status === "number" ? recognitionStatusMap[status] ?? "未知状态" : "未知状态";
}
// 相对文件地址通过接口代理访问,避免开发环境直接请求后端静态路径失败
function buildFileAccessUrl(fileUrl: string) {
if (
!fileUrl ||
/^(https?:)?\/\//i.test(fileUrl) ||
fileUrl.startsWith("blob:") ||
fileUrl.startsWith("data:")
) {
function buildFileAccessUrl(fileUrl) {
if (!fileUrl || /^(https?:)?\/\//i.test(fileUrl) || fileUrl.startsWith("blob:") || fileUrl.startsWith("data:")) {
return fileUrl;
}
if (fileUrl === BASE_URL || fileUrl.startsWith(`${BASE_URL}/`)) {
return fileUrl;
}
return `${BASE_URL}${fileUrl.startsWith("/") ? fileUrl : `/${fileUrl}`}`;
}
function normalizeDateTime(value: string) {
return value ? value.replace("T", " ") : undefined;
function normalizeDateTime(value) {
return value ? value.replace("T", " ") : void 0;
}
function normalizeRecognizeType(value: string) {
return value ? Number(value) : undefined;
function normalizeRecognizeType(value) {
return value ? Number(value) : void 0;
}
function normalizeWarningFilter(value: WarningFilterValue) {
function normalizeWarningFilter(value) {
if (!value) {
return undefined;
return void 0;
}
return value === "true";
}
function formatPosition(record: RosInspectionRecordLineImage) {
function formatPosition(record) {
if (typeof record.poseX !== "number" || typeof record.poseY !== "number") {
return "--";
}
return `X:${record.poseX.toFixed(2)} Y:${record.poseY.toFixed(2)}`;
}
onMounted(() => {
fetchAlarmList();
});
@@ -254,7 +189,6 @@ onMounted(() => {
/>
<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="fetchAlarmList">刷新</button>
</div>
<div class="toolbar-right"> {{ totalAlarms }} 条告警记录</div>
</div>
@@ -305,7 +239,11 @@ onMounted(() => {
<td>{{ alarm.time }}</td>
<td>{{ alarm.position }}</td>
<td>{{ alarm.result }}</td>
<td>{{ alarm.warning ? "是" : "否" }}</td>
<td>
<NTag :type="alarm.warning ? 'error' : 'success'" size="small">
{{ alarm.warning ? "是" : "否" }}
</NTag>
</td>
</tr>
</tbody>
</table>
@@ -338,6 +276,19 @@ onMounted(() => {
>
下一页
</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>
</section>
</div>

View File

@@ -1,31 +1,8 @@
<script lang="ts" setup>
<script setup>
import { computed, ref } from "vue";
import type { EChartsOption } from "echarts";
import Chart from "@/components/chart/index.vue";
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[] = [
const distributionRange = ref("近一周");
const reportData = [
{ 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-003", type: "墙体裂缝", robot: "巡检机器人 N3", project: "南山隧道", status: "处置完成", time: "2026-06-23 09:56:11" },
@@ -44,10 +21,9 @@ const reportData: AlarmReportRecord[] = [
{ 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-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: TrendRecord[] = [
const trendData = [
{ date: "06-09", project: "西江隧道", total: 1, closed: 1 },
{ date: "06-09", project: "青云隧道", total: 1, closed: 0 },
{ date: "06-09", project: "南山隧道", total: 1, closed: 1 },
@@ -92,53 +68,48 @@ const trendData: TrendRecord[] = [
{ date: "06-22", 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: 4, closed: 3 },
{ date: "06-23", project: "南山隧道", total: 4, closed: 3 }
];
const trendSeries = computed(() => {
const grouped = trendData.reduce<Record<string, { total: number; closed: number }>>(
const grouped = trendData.reduce(
(accumulator, item) => {
if (!accumulator[item.date]) {
accumulator[item.date] = { total: 0, closed: 0 };
}
accumulator[item.date].total += item.total;
accumulator[item.date].closed += item.closed;
return accumulator;
},
{},
{}
);
return Object.entries(grouped).map(([date, value]) => ({
date,
total: value.total,
closed: value.closed,
closed: value.closed
}));
});
const trendOption = computed<EChartsOption>(() => {
const trendOption = computed(() => {
const dates = trendSeries.value.map((item) => item.date);
const totalValues = trendSeries.value.map((item) => item.total);
const closedValues = trendSeries.value.map((item) => item.closed);
return {
grid: {
left: 24,
right: 16,
top: 24,
bottom: 28,
containLabel: true,
containLabel: true
},
tooltip: {
trigger: "axis",
backgroundColor: "rgba(6, 20, 42, 0.92)",
borderColor: "rgba(96, 208, 255, 0.24)",
textStyle: {
color: "#eefcff",
},
color: "#eefcff"
}
},
legend: {
show: false,
show: false
},
xAxis: {
type: "category",
@@ -146,35 +117,35 @@ const trendOption = computed<EChartsOption>(() => {
data: dates,
axisLine: {
lineStyle: {
color: "rgba(96, 188, 242, 0.2)",
},
color: "rgba(96, 188, 242, 0.2)"
}
},
axisLabel: {
color: "rgba(181, 230, 249, 0.74)",
fontSize: 11,
fontSize: 11
},
axisTick: {
show: false,
},
show: false
}
},
yAxis: {
type: "value",
splitNumber: 5,
axisLine: {
show: false,
show: false
},
axisTick: {
show: false,
show: false
},
axisLabel: {
color: "rgba(181, 230, 249, 0.62)",
fontSize: 11,
fontSize: 11
},
splitLine: {
lineStyle: {
color: "rgba(97, 189, 242, 0.12)",
},
},
color: "rgba(97, 189, 242, 0.12)"
}
}
},
series: [
{
@@ -186,16 +157,16 @@ const trendOption = computed<EChartsOption>(() => {
data: totalValues,
lineStyle: {
width: 3,
color: "#74ecff",
color: "#74ecff"
},
itemStyle: {
color: "#74ecff",
borderColor: "#081f3e",
borderWidth: 2,
borderWidth: 2
},
areaStyle: {
color: "rgba(116, 236, 255, 0.12)",
},
color: "rgba(116, 236, 255, 0.12)"
}
},
{
name: "闭环告警",
@@ -206,48 +177,44 @@ const trendOption = computed<EChartsOption>(() => {
data: closedValues,
lineStyle: {
width: 3,
color: "#62e0a8",
color: "#62e0a8"
},
itemStyle: {
color: "#62e0a8",
borderColor: "#081f3e",
borderWidth: 2,
borderWidth: 2
},
areaStyle: {
color: "rgba(98, 224, 168, 0.08)",
},
},
],
color: "rgba(98, 224, 168, 0.08)"
}
}
]
};
});
const typeStats = computed(() => {
const statsMap = new Map<string, number>();
const statsMap = /* @__PURE__ */ new Map();
reportData.forEach((item) => {
statsMap.set(item.type, (statsMap.get(item.type) ?? 0) + 1);
});
return Array.from(statsMap.entries()).map(([label, value]) => ({
label,
value,
value
}));
});
const typeRadarOption = computed<EChartsOption>(() => {
const typeRadarOption = computed(() => {
const maxValue = Math.max(...typeStats.value.map((item) => item.value), 1);
const indicators = typeStats.value.map((item) => ({
name: item.label,
max: maxValue,
max: maxValue
}));
return {
tooltip: {
trigger: "item",
backgroundColor: "rgba(6, 20, 42, 0.92)",
borderColor: "rgba(96, 208, 255, 0.24)",
textStyle: {
color: "#eefcff",
},
color: "#eefcff"
}
},
radar: {
radius: "68%",
@@ -256,23 +223,23 @@ const typeRadarOption = computed<EChartsOption>(() => {
splitNumber: 4,
axisName: {
color: "#dff7ff",
fontSize: 12,
fontSize: 12
},
axisLine: {
lineStyle: {
color: "rgba(97, 189, 242, 0.22)",
},
color: "rgba(97, 189, 242, 0.22)"
}
},
splitLine: {
lineStyle: {
color: "rgba(97, 189, 242, 0.16)",
},
color: "rgba(97, 189, 242, 0.16)"
}
},
splitArea: {
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: [
{
@@ -282,50 +249,47 @@ const typeRadarOption = computed<EChartsOption>(() => {
value: typeStats.value.map((item) => item.value),
name: "告警类型分布",
areaStyle: {
color: "rgba(73, 207, 255, 0.20)",
color: "rgba(73, 207, 255, 0.20)"
},
lineStyle: {
color: "#61dcff",
width: 2.5,
width: 2.5
},
itemStyle: {
color: "#8ff3ff",
borderColor: "#07203e",
borderWidth: 2,
borderWidth: 2
},
symbolSize: 8,
},
],
},
],
symbolSize: 8
}
]
}
]
};
});
const statusStats = computed(() => {
const order: AlarmStatus[] = ["待研判", "待处理", "处置完成", "误判"];
const order = ["待研判", "待处理", "处置完成", "误判"];
const total = reportData.length || 1;
return order.map((status) => {
const value = reportData.filter((item) => item.status === status).length;
return {
label: status,
value,
percent: `${Math.round((value / total) * 100)}%`,
percent: `${Math.round(value / total * 100)}%`
};
});
});
const statusPieOption = computed<EChartsOption>(() => ({
const statusPieOption = computed(() => ({
tooltip: {
trigger: "item",
backgroundColor: "rgba(6, 20, 42, 0.92)",
borderColor: "rgba(96, 208, 255, 0.24)",
textStyle: {
color: "#eefcff",
},
color: "#eefcff"
}
},
legend: {
show: false,
show: false
},
series: [
{
@@ -335,63 +299,50 @@ const statusPieOption = computed<EChartsOption>(() => ({
avoidLabelOverlap: false,
itemStyle: {
borderColor: "#081f3e",
borderWidth: 3,
borderWidth: 3
},
label: {
show: true,
color: "#dff7ff",
formatter: "{b}\n{d}%",
fontSize: 12,
fontSize: 12
},
labelLine: {
lineStyle: {
color: "rgba(181, 230, 249, 0.52)",
},
color: "rgba(181, 230, 249, 0.52)"
}
},
data: statusStats.value.map((item) => ({
name: item.label,
value: item.value,
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 now = new Date("2026-06-23T23:59:59");
const rangeDaysMap: Record<DistributionRange, number> = {
const now = /* @__PURE__ */ new Date("2026-06-23T23:59:59");
const rangeDaysMap = {
近一周: 7,
近一个月: 30,
近三个月: 90,
近三个月: 90
};
const threshold = new Date(
now.getTime() - rangeDaysMap[distributionRange.value] * 24 * 60 * 60 * 1000,
now.getTime() - rangeDaysMap[distributionRange.value] * 24 * 60 * 60 * 1e3
);
const data = reportData.filter((item) => new Date(item.time.replace(" ", "T")) >= threshold);
const statsMap = new Map<string, number>();
const statsMap = /* @__PURE__ */ new Map();
data.forEach((item) => {
statsMap.set(item.project, (statsMap.get(item.project) ?? 0) + 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,
value,
width: `${(value / maxValue) * 100}%`,
}))
.sort((a, b) => b.value - a.value);
width: `${value / maxValue * 100}%`
})).sort((a, b) => b.value - a.value);
});
</script>
@@ -468,7 +419,7 @@ const projectDistribution = computed(() => {
:key="range"
:class="['report-filter-tab', { active: distributionRange === range }]"
type="button"
@click="distributionRange = range as DistributionRange"
@click="distributionRange = range"
>
{{ range }}
</button>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,21 +1,6 @@
<script lang="ts" setup>
<script setup>
import { ref } from "vue";
interface PatrolAlarmRecord {
id: string;
type: string;
reportTime: string;
}
interface PatrolRecord {
id: string;
robot: string;
startTime: string;
endTime: string;
alarms: PatrolAlarmRecord[];
}
const patrolRecords: PatrolRecord[] = [
const patrolRecords = [
{
id: "PR-20260623-001",
robot: "巡检机器人 R1",
@@ -23,8 +8,8 @@ const patrolRecords: PatrolRecord[] = [
endTime: "2026-06-23 09:02:15",
alarms: [
{ 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",
@@ -32,8 +17,8 @@ const patrolRecords: PatrolRecord[] = [
startTime: "2026-06-23 09:20:08",
endTime: "2026-06-23 10:01:46",
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",
@@ -43,26 +28,23 @@ const patrolRecords: PatrolRecord[] = [
alarms: [
{ id: "AL-003", type: "墙体裂缝", reportTime: "2026-06-23 10:36:41" },
{ 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",
robot: "巡检机器人 R2",
startTime: "2026-06-23 13:15:17",
endTime: "2026-06-23 14:05:52",
alarms: [],
},
alarms: []
}
];
const dialogVisible = ref(false);
const activePatrolRecord = ref<PatrolRecord | null>(null);
function openAlarmDialog(record: PatrolRecord) {
const activePatrolRecord = ref(null);
function openAlarmDialog(record) {
activePatrolRecord.value = record;
dialogVisible.value = true;
}
function closeDialog() {
dialogVisible.value = false;
activePatrolRecord.value = null;

View File

@@ -1,75 +1,18 @@
<script lang="ts" setup>
<script setup>
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from "vue";
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 gridMode = ref<GridMode>(4);
const gridMode = ref(4);
const activeSlotIndex = ref(0);
const videoSlots = ref<Array<ProjectCameraItem | null>>([]);
const slotVideoRefs = ref<Array<HTMLVideoElement | null>>([]);
const slotStreamStates = ref<SlotStreamState[]>([]);
const videoGridBoardRef = ref<HTMLElement | null>(null);
const videoGridBoardStyle = ref<Record<string, string>>({});
const slotPeerConnections = new Map<number, RTCPeerConnection>();
const slotRequestIds = new Map<number, number>();
let videoGridResizeObserver: ResizeObserver | null = null;
const devices = ref<DeviceNode[]>([]);
const videoSlots = ref([]);
const slotVideoRefs = ref([]);
const slotStreamStates = ref([]);
const videoGridBoardRef = ref(null);
const videoGridBoardStyle = ref({});
const slotPeerConnections = /* @__PURE__ */ new Map();
const slotRequestIds = /* @__PURE__ */ new Map();
let videoGridResizeObserver = null;
const devices = ref([]);
const listLoading = ref(false);
const listError = ref("");
const playbackDialogVisible = ref(false);
@@ -77,141 +20,107 @@ const playbackStartAt = ref("2026-06-23T14:20");
const playbackEndAt = ref("2026-06-23T14:50");
const playbackProgress = ref(0);
const playbackPaused = ref(true);
const playbackPlayerRef = ref<HTMLVideoElement | null>(null);
const slotCountMap: Record<GridMode, number> = {
const playbackPlayerRef = ref(null);
const slotCountMap = {
1: 1,
4: 4,
9: 9,
9: 9
};
const currentProjectName = computed(() => devices.value[0]?.project || defaultProject);
const playableCameras = computed<ProjectCameraItem[]>(() =>
devices.value.flatMap((device) =>
device.cameras.map((camera) => ({
const playableCameras = computed(
() => devices.value.flatMap(
(device) => device.cameras.map((camera) => ({
...camera,
deviceId: device.id,
deviceName: device.name,
project: device.project,
})),
),
project: device.project
}))
)
);
const cameraTotal = computed(() => playableCameras.value.length);
const visibleSlots = computed(() => {
const targetCount = slotCountMap[gridMode.value];
return Array.from(
{ length: targetCount },
(_, index) => videoSlots.value[index] ?? null,
(_, index) => videoSlots.value[index] ?? null
);
});
const activeCamera = computed(() => visibleSlots.value[activeSlotIndex.value] ?? null);
const activeSlotLabel = computed(() => `画面 ${activeSlotIndex.value + 1}`);
const playbackRangeLabel = computed(() => {
const startLabel = playbackStartAt.value.replace("T", " ");
const endLabel = playbackEndAt.value.replace("T", " ");
return `${startLabel} - ${endLabel}`;
});
// 根据视频墙实际可用宽高计算宫格尺寸,保证每个画面都是 16:9 且不会溢出卡片
function updateVideoGridSize() {
const board = videoGridBoardRef.value;
if (!board) {
return;
}
const columns = Math.sqrt(gridMode.value);
const rows = columns;
const gap = 8;
const availableWidth = board.clientWidth - gap * (columns - 1);
const availableHeight = board.clientHeight - gap * (rows - 1);
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 cellHeight = cellWidth * (9 / 16);
videoGridBoardStyle.value = {
gridTemplateColumns: `repeat(${columns}, ${cellWidth}px)`,
gridAutoRows: `${cellHeight}px`,
gridAutoRows: `${cellHeight}px`
};
}
// 根据当前宫格数量补齐画面槽位,切换布局时保留已播放的视频
function ensureVideoSlotCount() {
const targetCount = slotCountMap[gridMode.value];
const previousCount = videoSlots.value.length;
for (let index = targetCount; index < previousCount; index += 1) {
stopSlotStream(index);
}
videoSlots.value = Array.from(
{ length: targetCount },
(_, index) => videoSlots.value[index] ?? null,
(_, index) => videoSlots.value[index] ?? null
);
slotVideoRefs.value = Array.from(
{ length: targetCount },
(_, index) => slotVideoRefs.value[index] ?? null,
(_, index) => slotVideoRefs.value[index] ?? null
);
slotStreamStates.value = Array.from(
{ length: targetCount },
(_, index) => slotStreamStates.value[index] ?? { loading: false, error: "" },
(_, index) => slotStreamStates.value[index] ?? { loading: false, error: "" }
);
if (activeSlotIndex.value >= targetCount) {
activeSlotIndex.value = targetCount - 1;
}
}
// 将接口的小车状态码转换为页面展示状态
function normalizeDeviceStatus(status?: number): DeviceStatus {
const statusMap: Record<number, DeviceStatus> = {
function normalizeDeviceStatus(status) {
const statusMap = {
0: "空闲",
1: "巡检",
2: "离线",
3: "充电中",
3: "充电中"
};
return statusMap[status ?? 2] ?? "离线";
}
// 根据小车视频地址生成可播放的相机节点
function buildCameraItem(
car: RosCarRecord,
deviceId: string,
type: CameraType,
url?: string,
cameraNo?: string,
): CameraItem | null {
function buildCameraItem(car, deviceId, type, url, cameraNo) {
if (!url) {
return null;
}
return {
id: `${deviceId}-${type}`,
name: `${type}相机`,
type,
status: normalizeDeviceStatus(car.status),
url,
cameraNo,
cameraNo
};
}
// 将设备列表接口的小车记录转换为左侧机器人树节点
function normalizeDeviceNode(car: RosCarRecord, index: number): DeviceNode {
function normalizeDeviceNode(car, index) {
const deviceId = car.id ?? `ROBOT-${index + 1}`;
const cameras = [
buildCameraItem(car, deviceId, "可见光", car.cameraWebrtcUrl, car.cameraNo),
buildCameraItem(car, deviceId, "红外", car.cameraWebrtcUrl2, car.cameraNo2),
].filter((camera): camera is CameraItem => Boolean(camera));
buildCameraItem(car, deviceId, "红外", car.cameraWebrtcUrl2, car.cameraNo2)
].filter((camera) => Boolean(camera));
return {
id: deviceId,
deviceNo: car.deviceNo,
@@ -219,23 +128,19 @@ function normalizeDeviceNode(car: RosCarRecord, index: number): DeviceNode {
project: car.projectName || defaultProject,
status: normalizeDeviceStatus(car.status),
battery: typeof car.batteryLevel === "number" ? `${car.batteryLevel}%` : "--",
cameras,
cameras
};
}
// 请求设备列表接口,加载左侧机器人及其可见光/红外相机
async function fetchDeviceList() {
listLoading.value = true;
listError.value = "";
try {
const response = (await getRosCarList({
const response = await getRosCarList({
page: 1,
limit: 999,
query: {},
})) as RosCarListResponse;
query: {}
});
const rows = response.data?.data ?? [];
devices.value = rows.map((car, index) => normalizeDeviceNode(car, index));
} catch (error) {
listError.value = "设备列表加载失败";
@@ -244,146 +149,112 @@ async function fetchDeviceList() {
listLoading.value = false;
}
}
// 选中右侧视频墙中的目标画面
function selectVideoSlot(index: number) {
function selectVideoSlot(index) {
activeSlotIndex.value = index;
}
// 停止全部画面的 WebRTC 拉流,用于页面卸载时释放资源
function stopAllSlotStreams() {
videoSlots.value.forEach((_, index) => stopSlotStream(index));
}
// 停止指定画面的 WebRTC 拉流并释放播放器资源
function stopSlotStream(index: number, clearState = true) {
function stopSlotStream(index, clearState = true) {
slotRequestIds.set(index, (slotRequestIds.get(index) ?? 0) + 1);
if (clearState) {
slotStreamStates.value[index] = { loading: false, error: "" };
}
const peerConnection = slotPeerConnections.get(index);
if (peerConnection) {
peerConnection.close();
slotPeerConnections.delete(index);
}
const video = slotVideoRefs.value[index];
if (video) {
video.srcObject = null;
}
}
// 将设备返回的 webrtc 播放地址转换为 ZLMediaKit WebRTC 播放接口地址
function buildWebrtcApiUrl(streamUrl: string) {
function buildWebrtcApiUrl(streamUrl) {
const normalizedUrl = streamUrl.replace(/^webrtc:\/\//, "https://");
const url = new URL(normalizedUrl);
return `${url.origin}${url.pathname}${url.search}`;
}
// 等待本地 ICE 候选收集完成,确保发给流媒体服务的 offer 信息完整
function waitForIceGatheringComplete(peerConnection: RTCPeerConnection) {
function waitForIceGatheringComplete(peerConnection) {
if (peerConnection.iceGatheringState === "complete") {
return Promise.resolve();
}
return new Promise<void>((resolve) => {
return new Promise((resolve) => {
const handleIceGatheringStateChange = () => {
if (peerConnection.iceGatheringState === "complete") {
peerConnection.removeEventListener("icegatheringstatechange", handleIceGatheringStateChange);
resolve();
}
};
peerConnection.addEventListener("icegatheringstatechange", handleIceGatheringStateChange);
});
}
// 收集视频墙每个画面对应的 video 元素,供 WebRTC ontrack 写入媒体流
function setSlotVideoRef(element: Element | { $el?: Element } | null, index: number) {
function setSlotVideoRef(element, index) {
const target = element instanceof Element ? element : element?.$el;
slotVideoRefs.value[index] = target instanceof HTMLVideoElement ? target : null;
}
// 通过 WebRTC offer/answer 将指定相机拉流到选中的画面槽位
async function playSlotStream(index: number, camera: ProjectCameraItem) {
async function playSlotStream(index, camera) {
stopSlotStream(index);
if (!window.RTCPeerConnection) {
slotStreamStates.value[index] = { loading: false, error: "当前浏览器不支持 WebRTC 播放" };
return;
}
const video = slotVideoRefs.value[index];
if (!video) {
slotStreamStates.value[index] = { loading: false, error: "播放器未准备好" };
return;
}
const requestId = slotRequestIds.get(index) ?? 0;
const peerConnection = new RTCPeerConnection();
slotPeerConnections.set(index, peerConnection);
slotStreamStates.value[index] = { loading: true, error: "" };
peerConnection.addTransceiver("video", { direction: "recvonly" });
peerConnection.addTransceiver("audio", { direction: "recvonly" });
peerConnection.ontrack = (event) => {
if (requestId !== slotRequestIds.get(index)) {
return;
}
video.srcObject = event.streams[0];
};
try {
const offer = await peerConnection.createOffer();
await peerConnection.setLocalDescription(offer);
await waitForIceGatheringComplete(peerConnection);
const response = await fetch(buildWebrtcApiUrl(camera.url), {
method: "POST",
headers: {
"Content-Type": "application/sdp",
"Content-Type": "application/sdp"
},
body: peerConnection.localDescription?.sdp ?? offer.sdp,
body: peerConnection.localDescription?.sdp ?? offer.sdp
});
if (!response.ok) {
throw new Error(`拉流失败:${response.status}`);
throw new Error(`拉流失败\${response.status}`);
}
const responseText = await response.text();
let answer = responseText;
try {
const responseData = JSON.parse(responseText) as { code?: number; sdp?: string; msg?: string };
const responseData = JSON.parse(responseText);
if (responseData.code && responseData.code !== 0) {
throw new Error(responseData.msg || "视频流拉取失败");
}
answer = responseData.sdp || responseText;
} catch (error) {
if (responseText.trim().startsWith("{")) {
throw error;
}
}
if (requestId !== slotRequestIds.get(index)) {
return;
}
await peerConnection.setRemoteDescription({
type: "answer",
sdp: answer,
sdp: answer
});
} catch (error) {
if (requestId === slotRequestIds.get(index)) {
slotStreamStates.value[index] = {
loading: false,
error: error instanceof Error ? error.message : "视频流拉取失败",
error: error instanceof Error ? error.message : "视频流拉取失败"
};
stopSlotStream(index, false);
}
@@ -391,135 +262,98 @@ async function playSlotStream(index: number, camera: ProjectCameraItem) {
if (requestId === slotRequestIds.get(index)) {
slotStreamStates.value[index] = {
...slotStreamStates.value[index],
loading: false,
loading: false
};
}
}
}
// 关闭指定画面并释放对应 WebRTC 拉流资源
function closeVideoSlot(index: number) {
function closeVideoSlot(index) {
stopSlotStream(index);
videoSlots.value[index] = null;
if (activeSlotIndex.value === index) {
activeSlotIndex.value = index;
}
}
// 将左侧相机播放到当前选中的右侧画面
async function playCamera(camera: ProjectCameraItem) {
async function playCamera(camera) {
const targetIndex = activeSlotIndex.value;
videoSlots.value[targetIndex] = camera;
await nextTick();
await playSlotStream(targetIndex, camera);
}
// 切换视频墙宫格布局
async function setGridMode(mode: GridMode) {
async function setGridMode(mode) {
gridMode.value = mode;
await nextTick();
updateVideoGridSize();
}
// 同步录像回放播放器进度和暂停状态
function syncPlaybackProgress() {
const video = playbackPlayerRef.value;
if (!video) {
playbackProgress.value = 0;
playbackPaused.value = true;
return;
}
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;
}
// 从头开始播放当前回放视频
function startPlayback() {
const video = playbackPlayerRef.value;
if (!video) {
return;
}
video.currentTime = 0;
void video.play();
playbackPaused.value = false;
syncPlaybackProgress();
}
// 打开当前画面的录像回放弹窗
async function openPlaybackDialog() {
if (!activeCamera.value) {
return;
}
playbackDialogVisible.value = true;
playbackProgress.value = 0;
playbackPaused.value = true;
await nextTick();
startPlayback();
}
// 关闭录像回放弹窗并暂停播放器
function closePlaybackDialog() {
playbackPlayerRef.value?.pause();
playbackDialogVisible.value = false;
playbackPaused.value = true;
}
// 切换录像回放的播放和暂停状态
function togglePlaybackPause() {
const video = playbackPlayerRef.value;
if (!video) {
return;
}
if (video.paused) {
void video.play();
} else {
video.pause();
}
syncPlaybackProgress();
}
// 按秒数快进或快退录像回放
function seekPlayback(offsetSeconds: number) {
function seekPlayback(offsetSeconds) {
const video = playbackPlayerRef.value;
if (!video) {
return;
}
const duration = Number.isFinite(video.duration) ? video.duration : 0;
const targetTime = Math.max(0, Math.min(duration, video.currentTime + offsetSeconds));
video.currentTime = targetTime;
syncPlaybackProgress();
}
// 根据拖动条位置更新录像回放播放进度
function updatePlaybackProgress() {
const video = playbackPlayerRef.value;
if (!video) {
return;
}
const duration = Number.isFinite(video.duration) ? video.duration : 0;
if (duration <= 0) {
return;
}
video.currentTime = (playbackProgress.value / 100) * duration;
video.currentTime = playbackProgress.value / 100 * duration;
}
watch(gridMode, async () => {
ensureVideoSlotCount();
await nextTick();
@@ -528,7 +362,6 @@ watch(gridMode, async () => {
onMounted(() => {
void fetchDeviceList();
updateVideoGridSize();
if (videoGridBoardRef.value) {
videoGridResizeObserver = new ResizeObserver(updateVideoGridSize);
videoGridResizeObserver.observe(videoGridBoardRef.value);
@@ -618,7 +451,7 @@ onBeforeUnmount(() => {
:key="mode"
:class="['grid-switch-btn', { active: gridMode === mode }]"
type="button"
@click="setGridMode(mode as GridMode)"
@click="setGridMode(mode)"
>
{{ mode }} 宫格
</button>

View File

@@ -1,27 +1,19 @@
export type AlertPhotoScene = "cable" | "heat" | "crack";
export function createAlertPhoto(scene: AlertPhotoScene, accent: string) {
const sceneMarkup =
scene === "cable"
? `
function createAlertPhoto(scene, accent) {
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="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" />
`
: scene === "heat"
? `
` : scene === "heat" ? `
<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)" />
<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="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="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" />
`;
const svg = `
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 360">
<defs>
@@ -61,21 +53,22 @@ export function createAlertPhoto(scene: AlertPhotoScene, accent: string) {
<rect x="20" y="20" width="600" height="320" rx="16" fill="none" stroke="rgba(110,224,255,0.12)" stroke-width="2" />
</svg>
`;
return `data:image/svg+xml;charset=UTF-8,${encodeURIComponent(svg)}`;
}
export const recentAlertPhotoSources = {
const recentAlertPhotoSources = {
"线缆破损": createAlertPhoto("cable", "#ff8f79"),
"电缆表面破损": createAlertPhoto("cable", "#ff8f79"),
"桥架断裂": createAlertPhoto("crack", "#89d8ff"),
"隧道积水": createAlertPhoto("heat", "#ffc86c"),
"局部高温": createAlertPhoto("heat", "#ffc86c"),
"墙体裂缝": createAlertPhoto("crack", "#89d8ff"),
} as const;
"墙体裂缝": createAlertPhoto("crack", "#89d8ff")
};
const fallbackAlertPhoto = recentAlertPhotoSources["墙体裂缝"];
export function getRecentAlertPhoto(type: string) {
return recentAlertPhotoSources[type as keyof typeof recentAlertPhotoSources] ?? fallbackAlertPhoto;
function getRecentAlertPhoto(type) {
return recentAlertPhotoSources[type] ?? fallbackAlertPhoto;
}
export {
createAlertPhoto,
getRecentAlertPhoto,
recentAlertPhotoSources
};

View File

@@ -1,5 +1,5 @@
<script lang="ts" setup>
import SwitchPageBtn from "@/views/dashboard1/components/btn";
<script setup>
</script>
<template>

View File

@@ -0,0 +1,20 @@
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>;
}
});

View File

@@ -1,26 +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}></li>
})
}
</ul>
<div class='chart__name'>xx</div>
</div>
);
},
});

View File

@@ -1,20 +1,16 @@
<script lang="ts" setup>
<script setup>
import { computed, ref } from "vue";
const animationSvgRef = ref();
const config = {
count: 16,
initR: 2,
gap: 0.2,
dur: 8,
startY: 266.5,
startY: 266.5
};
function generateNumber() {
return (Math.random() * 10).toFixed(1) + "Mb/s";
}
const polylineComputed = computed(() => {
const arr = [];
let startY = config.startY;
@@ -22,28 +18,25 @@ const polylineComputed = computed(() => {
let startX = 465;
let endX = 1820;
const points = `${startX},${startY} 846,${startY} 1280,634 ${endX},634`;
const textY = startY - 29.5;
const text1 = { x: startX + 137, y: textY, value: `${generateNumber()}` };
const text2 = {
x: startX + 137 + 147,
y: textY,
value: `${generateNumber()}`,
value: `${generateNumber()}`
};
const text3 = {};
const value = {
points,
text1,
text2,
text3,
text3
};
arr.push(value);
startY += 180;
}
return arr;
});
const circleComputed = computed(() => {
const circles = [];
let startY = config.startY;
@@ -60,7 +53,7 @@ const circleComputed = computed(() => {
r: r.toFixed(1),
startX,
startY,
endX,
endX
};
circles.push(circle);
endX = 1820;

View File

@@ -1,33 +1,24 @@
<script lang="ts" setup>
<script setup>
import { computed, reactive, ref } from "vue";
import { useRoute, useRouter } from "vue-router";
import { useUserStore } from "@/store/modules/user.js";
import { encryptPasswordPayload } from '@/utils/passwordEncrypt.js'
import { encryptPasswordPayload } from "@/utils/passwordEncrypt.js";
const route = useRoute();
const router = useRouter();
const userStore = useUserStore();
const form = reactive({
username: "",
password: "",
password: ""
});
const loading = ref(false);
const errorMessage = ref("");
const redirectPath = computed(() => {
const redirect = route.query.redirect;
if (
typeof redirect === "string" &&
redirect.startsWith("/") &&
!redirect.startsWith("//")
) {
if (typeof redirect === "string" && redirect.startsWith("/") && !redirect.startsWith("//")) {
return redirect;
}
return "/dashboard/integrated-center";
});
// 校验登录表单,避免空账号或空密码提交到后端。
function validateLoginForm() {
if (!form.username.trim()) {
errorMessage.value = "请输入账号";
@@ -39,23 +30,18 @@ function validateLoginForm() {
}
return true;
}
// 提交登录信息,成功后回到登录前访问的页面。
async function handleLogin() {
if (!validateLoginForm() || loading.value) {
return;
}
loading.value = true;
errorMessage.value = "";
try {
const encryptedForm = await encryptPasswordPayload(form, ['password'])
await userStore.login(encryptedForm)
const encryptedForm = await encryptPasswordPayload(form, ["password"]);
await userStore.login(encryptedForm);
router.replace(redirectPath.value);
} catch (error) {
errorMessage.value =
error instanceof Error ? error.message : "登录失败,请检查账号或密码";
errorMessage.value = error instanceof Error ? error.message : "登录失败\,请检查账号或密码";
} finally {
loading.value = false;
}

8
src/vite-env.d.ts vendored
View File

@@ -1,8 +0,0 @@
/// <reference types="vite/client" />
/// <reference types="vue/jsx" />
declare module "*.vue" {
import { DefineComponent } from "vue";
const component: DefineComponent<{}, {}, any>;
export default component;
}

View File

@@ -1,21 +0,0 @@
{
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
"module": "ESNext",
"moduleResolution": "Node",
"strict": true,
"jsx": "preserve",
"resolveJsonModule": true,
"isolatedModules": true,
"esModuleInterop": true,
"lib": ["ESNext", "DOM"],
"skipLibCheck": true,
"noEmit": true,
"baseUrl": "./",
"paths": {
"@/*": ["src/*"]
}
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue", "src/api/rosCar.ts"]
}

41
vite.config.js Normal file
View File

@@ -0,0 +1,41 @@
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"] : []
}
};
};

View File

@@ -1,44 +0,0 @@
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
import { resolve } from 'path'
// import { viteSingleFile } from 'vite-plugin-singlefile'
// https://vitejs.dev/config/
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'] : [],
}
})
}