first commit
26
.gitignore
vendored
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
pnpm-lock.yaml
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
3
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"]
|
||||||
|
}
|
||||||
3
README.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
## 预览图
|
||||||
|
|
||||||
|

|
||||||
BIN
doc/images/1.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
13
index.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Vite + Vue + TS</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
2274
package-lock.json
generated
Normal file
28
package.json
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"name": "dashboard-demo",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vue-tsc && vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"format": "prettier --write ."
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "^20.4.4",
|
||||||
|
"echarts": "^5.4.3",
|
||||||
|
"echarts-liquidfill": "^3.1.0",
|
||||||
|
"ol": "^7.4.0",
|
||||||
|
"prettier": "^3.0.0",
|
||||||
|
"vue": "^3.2.47"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@vitejs/plugin-vue": "^4.1.0",
|
||||||
|
"less": "^4.1.3",
|
||||||
|
"typescript": "^4.9.3",
|
||||||
|
"vite": "^4.2.0",
|
||||||
|
"vue-tsc": "^1.2.0",
|
||||||
|
"@vitejs/plugin-vue-jsx": "^1.3.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
10
prettierrc.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"semi": true,
|
||||||
|
"singleQuote": true,
|
||||||
|
"quoteProps": "as-needed",
|
||||||
|
"bracketSpacing": true,
|
||||||
|
"arrowParens": "always",
|
||||||
|
"trailingComma": "all",
|
||||||
|
"tabWidth": 2,
|
||||||
|
"jsxBracketSameLine": true
|
||||||
|
}
|
||||||
1
public/vite.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
65
src/App.vue
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, onBeforeUnmount, onMounted, ref } from "vue";
|
||||||
|
import dashboard1 from "@/views/dashboard1/index.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})`,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
updateScreenAdapter();
|
||||||
|
window.addEventListener("resize", updateScreenAdapter);
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
window.removeEventListener("resize", updateScreenAdapter);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="screen-adapter">
|
||||||
|
<div class="screen-adapter__inner" :style="adapterStyle">
|
||||||
|
<Component :is="dashboard1" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.screen-adapter {
|
||||||
|
position: relative;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.screen-adapter__inner {
|
||||||
|
position: absolute;
|
||||||
|
transform-origin: left top;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
BIN
src/assets/map-material.jpg
Normal file
|
After Width: | Height: | Size: 26 KiB |
56
src/components/chart/index.vue
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { 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>();
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
option: {
|
||||||
|
type: Object as () => EChartsOption | {},
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
function applyOption(option?: EChartsOption | Record<string, never>) {
|
||||||
|
if (!chart || !option) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
chart.clear();
|
||||||
|
chart.setOption(option);
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
chart = echarts.init(chartRef.value, "t-theme");
|
||||||
|
applyOption(props.option);
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.option,
|
||||||
|
(value) => {
|
||||||
|
applyOption(value);
|
||||||
|
},
|
||||||
|
{ deep: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
if (chart) {
|
||||||
|
chart.dispose();
|
||||||
|
chart = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div ref="chartRef" class="t-chart" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.t-chart {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
104
src/config/echarts.config.ts
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
const color = [
|
||||||
|
{
|
||||||
|
type: "linear",
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
x2: 1,
|
||||||
|
y2: 1,
|
||||||
|
colorStops: [
|
||||||
|
{ offset: 0, color: "#94c7fd" },
|
||||||
|
{ offset: 0.5, color: "#3b99fb" },
|
||||||
|
{ offset: 1, color: "#004ca0" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "linear",
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
x2: 1,
|
||||||
|
y2: 1,
|
||||||
|
colorStops: [
|
||||||
|
{ offset: 0, color: "#b070c0" },
|
||||||
|
{ offset: 0.5, color: "#b070c0" },
|
||||||
|
{ offset: 1, color: "#5d235d" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "linear",
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
x2: 1,
|
||||||
|
y2: 1,
|
||||||
|
colorStops: [
|
||||||
|
{ offset: 0, color: "#7eedd6" },
|
||||||
|
{ offset: 0.5, color: "#3be8bb" },
|
||||||
|
{ offset: 1, color: "#00b38a" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "linear",
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
x2: 0,
|
||||||
|
y2: 1,
|
||||||
|
colorStops: [
|
||||||
|
{ offset: 0, color: "#fff1f0" },
|
||||||
|
{ offset: 0.5, color: "#ff9494" },
|
||||||
|
{ offset: 1, color: "#ff4d4f" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "linear",
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
x2: 0,
|
||||||
|
y2: 1,
|
||||||
|
colorStops: [
|
||||||
|
{ offset: 0, color: "#fffbe6" },
|
||||||
|
{ offset: 0.5, color: "#ffe58f" },
|
||||||
|
{ offset: 1, color: "#ffbb96" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "linear",
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
x2: 0,
|
||||||
|
y2: 1,
|
||||||
|
colorStops: [
|
||||||
|
{ offset: 0, color: "#ff3333" },
|
||||||
|
{ offset: 0.5, color: "#ff0000" },
|
||||||
|
{ offset: 1, color: "#cc0000" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "linear",
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
x2: 0,
|
||||||
|
y2: 1,
|
||||||
|
colorStops: [
|
||||||
|
{ offset: 0, color: "#ffe6f0" },
|
||||||
|
{ offset: 0.5, color: "#ffb3d9" },
|
||||||
|
{ offset: 1, color: "#ff85c0" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default {
|
||||||
|
color,
|
||||||
|
textStyle: {
|
||||||
|
fontSize: 12,
|
||||||
|
color: "#fff",
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
textStyle: {
|
||||||
|
color: "#fff",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
textStyle: {
|
||||||
|
align: "left",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
36
src/hooks/useLayerStack.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
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) {
|
||||||
|
const vectorSource = new VectorSource({
|
||||||
|
url: "https://geo.datav.aliyun.com/areas_v3/bound/330000.json",
|
||||||
|
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();
|
||||||
|
geometry.translate(0, -0.04 * i);
|
||||||
|
return new Style({
|
||||||
|
fill: new Fill({
|
||||||
|
color: "rgba(176, 166, 132,0.6)",
|
||||||
|
}),
|
||||||
|
stroke: new Stroke({
|
||||||
|
color: "rgba(176, 166, 132,0.6)",
|
||||||
|
width: 1,
|
||||||
|
}),
|
||||||
|
geometry,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
mapInstance.addLayer(layer);
|
||||||
|
}
|
||||||
|
}
|
||||||
111
src/hooks/useMap.ts
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
import { Map, View } from "ol";
|
||||||
|
import { onMounted } from "vue";
|
||||||
|
import { GeoJSON } from "ol/format";
|
||||||
|
import VectorSource from "ol/source/Vector";
|
||||||
|
import VectorLayer from "ol/layer/Vector";
|
||||||
|
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) {
|
||||||
|
const mapInstance = new Map({
|
||||||
|
view: new View({
|
||||||
|
projection: "EPSG:4326",
|
||||||
|
zoom: 12,
|
||||||
|
center: [0, 0],
|
||||||
|
}),
|
||||||
|
controls: [],
|
||||||
|
interactions: defaults({
|
||||||
|
dragPan: false,
|
||||||
|
mouseWheelZoom: false,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const vectorSource = new VectorSource({
|
||||||
|
url: "https://geo.datav.aliyun.com/areas_v3/bound/330000_full.json",
|
||||||
|
format: new GeoJSON(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const customStyle = new Style({
|
||||||
|
renderer: function (pixelCoordinates, state) {
|
||||||
|
const context = state.context;
|
||||||
|
const multiPolygon = state.geometry.clone() as MultiPolygon;
|
||||||
|
// @ts-ignore
|
||||||
|
multiPolygon.setCoordinates(pixelCoordinates);
|
||||||
|
const extent = multiPolygon.getExtent();
|
||||||
|
const width = getWidth(extent);
|
||||||
|
const height = getHeight(extent);
|
||||||
|
const flag = state.feature.get("material");
|
||||||
|
if (!flag || height < 1 || width < 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
context.save();
|
||||||
|
const renderContext = toContext(context, {
|
||||||
|
pixelRatio: 1,
|
||||||
|
});
|
||||||
|
renderContext.setFillStrokeStyle(
|
||||||
|
new Fill(),
|
||||||
|
new Stroke({
|
||||||
|
color: "rgba(255,255,255,0.8)",
|
||||||
|
width: 3,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
renderContext.drawMultiPolygon(multiPolygon);
|
||||||
|
context.clip();
|
||||||
|
const bottomLeft = getBottomLeft(extent);
|
||||||
|
const left = bottomLeft[0];
|
||||||
|
const bottom = bottomLeft[1];
|
||||||
|
context.drawImage(flag, left, bottom, width, height);
|
||||||
|
context.restore();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const layer = new VectorLayer({
|
||||||
|
source: vectorSource,
|
||||||
|
style: (feature) => {
|
||||||
|
return [
|
||||||
|
customStyle,
|
||||||
|
new Style({
|
||||||
|
text: new Text({
|
||||||
|
text: feature.get("name"),
|
||||||
|
fill: new Fill({
|
||||||
|
color: "#fff",
|
||||||
|
}),
|
||||||
|
scale: 1.5,
|
||||||
|
}),
|
||||||
|
zIndex: 1,
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
vectorSource.on("addfeature", function (event) {
|
||||||
|
const feature = event.feature;
|
||||||
|
const img = new Image();
|
||||||
|
img.src = materialJpg;
|
||||||
|
img.onload = function () {
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
14
src/main.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { createApp } from "vue";
|
||||||
|
import "./style.less";
|
||||||
|
import App from "./App.vue";
|
||||||
|
import Chart from "./components/chart/index.vue";
|
||||||
|
import * as echarts from "echarts";
|
||||||
|
import echartsConfig from "./config/echarts.config";
|
||||||
|
|
||||||
|
echarts.registerTheme("t-theme", echartsConfig);
|
||||||
|
|
||||||
|
const app = createApp(App);
|
||||||
|
|
||||||
|
app.component("TChart", Chart);
|
||||||
|
|
||||||
|
app.mount("#app");
|
||||||
45
src/style.less
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
:root {
|
||||||
|
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||||
|
line-height: 1.5;
|
||||||
|
font-weight: 400;
|
||||||
|
|
||||||
|
color-scheme: light dark;
|
||||||
|
color: rgba(255, 255, 255, 0.87);
|
||||||
|
background-color: #242424;
|
||||||
|
font-synthesis: none;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
-webkit-text-size-adjust: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#app {
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: light) {
|
||||||
|
:root {
|
||||||
|
color: #213547;
|
||||||
|
background-color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
color: #747bff;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
}
|
||||||
|
}
|
||||||
19
src/utils/index.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
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) {
|
||||||
|
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||||
|
}
|
||||||
|
const numbers = [];
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
const randomNum = Math.floor(Math.random() * (max - min + 1)) + min;
|
||||||
|
numbers.push(randomNum);
|
||||||
|
}
|
||||||
|
return numbers;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { generateNumbers };
|
||||||
BIN
src/views/dashboard1/assets/01.png
Normal file
|
After Width: | Height: | Size: 194 KiB |
BIN
src/views/dashboard1/assets/01.webp
Normal file
|
After Width: | Height: | Size: 362 KiB |
BIN
src/views/dashboard1/assets/上升粒子1.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
src/views/dashboard1/assets/上升粒子1.webp
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
src/views/dashboard1/assets/上升粒子2.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
src/views/dashboard1/assets/上升粒子2.webp
Normal file
|
After Width: | Height: | Size: 50 KiB |
BIN
src/views/dashboard1/assets/卡片背景.png
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
src/views/dashboard1/assets/头部动画.png
Normal file
|
After Width: | Height: | Size: 444 KiB |
BIN
src/views/dashboard1/assets/头部动画.webp
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
BIN
src/views/dashboard1/assets/头部动画_右侧点.png
Normal file
|
After Width: | Height: | Size: 58 KiB |
BIN
src/views/dashboard1/assets/头部动画_右侧点.webp
Normal file
|
After Width: | Height: | Size: 110 KiB |
BIN
src/views/dashboard1/assets/头部动画_左侧点.png
Normal file
|
After Width: | Height: | Size: 60 KiB |
BIN
src/views/dashboard1/assets/头部动画_左侧点.webp
Normal file
|
After Width: | Height: | Size: 112 KiB |
BIN
src/views/dashboard1/assets/头部吊坠.png
Normal file
|
After Width: | Height: | Size: 43 KiB |
BIN
src/views/dashboard1/assets/底座_圆形_静态图片.png
Normal file
|
After Width: | Height: | Size: 72 KiB |
BIN
src/views/dashboard1/assets/底座_静态图片.png
Normal file
|
After Width: | Height: | Size: 9.2 KiB |
BIN
src/views/dashboard1/assets/底部动画.png
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
src/views/dashboard1/assets/底部动画.webp
Normal file
|
After Width: | Height: | Size: 97 KiB |
BIN
src/views/dashboard1/assets/循环背景动画.mp4
Normal file
BIN
src/views/dashboard1/assets/按钮未选中.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
src/views/dashboard1/assets/按钮未选中1.png
Normal file
|
After Width: | Height: | Size: 733 B |
BIN
src/views/dashboard1/assets/按钮选中.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
src/views/dashboard1/assets/按钮选中1.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
src/views/dashboard1/assets/按钮选中2.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
src/views/dashboard1/assets/按钮选中3.png
Normal file
|
After Width: | Height: | Size: 488 B |
BIN
src/views/dashboard1/assets/整体边框_静态图片.png
Normal file
|
After Width: | Height: | Size: 204 KiB |
BIN
src/views/dashboard1/assets/标题1_静态背景.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
61
src/views/dashboard1/components/btn/index.module.less
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
.btn {
|
||||||
|
width: 130px;
|
||||||
|
height: 35px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.normal {
|
||||||
|
background-image: url("../../assets/按钮未选中.png"), url("../../assets/按钮未选中1.png");
|
||||||
|
background-repeat: no-repeat, no-repeat;
|
||||||
|
background-position: 4px 0, 0 4px;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 35px;
|
||||||
|
|
||||||
|
.text {
|
||||||
|
background-image: -webkit-linear-gradient(top, #fff, #fff, #69a2d8, #69a2d8, #69a2d8);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.right {
|
||||||
|
.text {
|
||||||
|
transform: rotateY(-180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
transform: rotateY(180deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background-image: url("../../assets/按钮选中.png"),
|
||||||
|
url("../../assets/按钮选中2.png"),
|
||||||
|
url("../../assets/按钮选中3.png"),
|
||||||
|
url("../../assets/按钮选中1.png");
|
||||||
|
background-repeat: no-repeat, no-repeat, no-repeat, no-repeat;
|
||||||
|
background-size: 100%, 100%, 100% 100%, 100% 100%;
|
||||||
|
background-position: 0 2.5px, -2px 0, center, top;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 35px;
|
||||||
|
|
||||||
|
.text {
|
||||||
|
background-image: -webkit-linear-gradient(top, rgb(157, 195, 234), #fff, rgb(96, 162, 225));
|
||||||
|
}
|
||||||
|
|
||||||
|
&.left {
|
||||||
|
.text {
|
||||||
|
transform: rotateY(-180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
transform: rotateY(180deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.text {
|
||||||
|
font-weight: 600;
|
||||||
|
text-align: center;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
51
src/views/dashboard1/hooks/useLeftChart1.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import { onBeforeUnmount, ref } from "vue";
|
||||||
|
import { EChartsOption } from "echarts";
|
||||||
|
import { generateNumbers } from "@/utils";
|
||||||
|
|
||||||
|
export default function () {
|
||||||
|
const option = ref<EChartsOption>({});
|
||||||
|
|
||||||
|
function refresh() {
|
||||||
|
option.value = {
|
||||||
|
xAxis: {
|
||||||
|
type: "category",
|
||||||
|
data: ["周一", "周二", "周三", "周四", "周五", "周六", "周日"],
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
type: "value",
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
left: "12%",
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
data: generateNumbers(1, 300, 7),
|
||||||
|
type: "line",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
data: generateNumbers(1, 300, 7),
|
||||||
|
type: "line",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
data: generateNumbers(1, 300, 7),
|
||||||
|
type: "line",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh();
|
||||||
|
|
||||||
|
const timer = setInterval(() => {
|
||||||
|
refresh();
|
||||||
|
}, 3000);
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
clearInterval(timer);
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
option,
|
||||||
|
refresh,
|
||||||
|
};
|
||||||
|
}
|
||||||
66
src/views/dashboard1/hooks/useLeftChart2.ts
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import { onBeforeUnmount, ref } from "vue";
|
||||||
|
import { EChartsOption } from "echarts";
|
||||||
|
import { generateNumbers } from "@/utils";
|
||||||
|
|
||||||
|
export default function () {
|
||||||
|
const option = ref<EChartsOption>({});
|
||||||
|
|
||||||
|
function refresh() {
|
||||||
|
option.value = {
|
||||||
|
tooltip: {
|
||||||
|
trigger: "item",
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
top: "5%",
|
||||||
|
left: "center",
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: "Access From",
|
||||||
|
type: "pie",
|
||||||
|
radius: ["40%", "70%"],
|
||||||
|
avoidLabelOverlap: false,
|
||||||
|
itemStyle: {
|
||||||
|
borderColor: "#fff",
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
show: false,
|
||||||
|
position: "center",
|
||||||
|
},
|
||||||
|
emphasis: {
|
||||||
|
label: {
|
||||||
|
show: true,
|
||||||
|
fontSize: 30,
|
||||||
|
fontWeight: "bold",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
labelLine: {
|
||||||
|
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: "产品五" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh();
|
||||||
|
|
||||||
|
const timer = setInterval(() => {
|
||||||
|
refresh();
|
||||||
|
}, 3000);
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
clearInterval(timer);
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
option,
|
||||||
|
refresh,
|
||||||
|
};
|
||||||
|
}
|
||||||
74
src/views/dashboard1/hooks/useLeftChart3.ts
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
import { onBeforeUnmount, ref } from "vue";
|
||||||
|
import { EChartsOption } from "echarts";
|
||||||
|
import { generateNumbers } from "@/utils";
|
||||||
|
|
||||||
|
export default function () {
|
||||||
|
const option = ref<EChartsOption>({});
|
||||||
|
|
||||||
|
function refresh() {
|
||||||
|
option.value = {
|
||||||
|
tooltip: {
|
||||||
|
trigger: "item",
|
||||||
|
formatter: "{a} <br/>{b} : {c}%",
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: "Funnel",
|
||||||
|
type: "funnel",
|
||||||
|
left: "center",
|
||||||
|
width: "100%",
|
||||||
|
min: 0,
|
||||||
|
max: 1000,
|
||||||
|
minSize: "0%",
|
||||||
|
maxSize: "100%",
|
||||||
|
sort: "descending",
|
||||||
|
gap: 2,
|
||||||
|
label: {
|
||||||
|
show: true,
|
||||||
|
position: "inside",
|
||||||
|
},
|
||||||
|
labelLine: {
|
||||||
|
length: 10,
|
||||||
|
lineStyle: {
|
||||||
|
width: 1,
|
||||||
|
type: "solid",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
itemStyle: {
|
||||||
|
borderColor: "#fff",
|
||||||
|
borderWidth: 1,
|
||||||
|
},
|
||||||
|
emphasis: {
|
||||||
|
label: {
|
||||||
|
fontSize: 20,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data: [
|
||||||
|
{ value: generateNumbers(500, 1000), name: "访问量" },
|
||||||
|
{ value: generateNumbers(500, 1000), name: "注册量" },
|
||||||
|
{ value: generateNumbers(500, 1000), name: "浏览量" },
|
||||||
|
{ value: generateNumbers(200, 500), name: "加入购物车" },
|
||||||
|
{ value: generateNumbers(100, 800), name: "购买" },
|
||||||
|
{ value: generateNumbers(1, 20), name: "退货" },
|
||||||
|
{ value: generateNumbers(1, 20), name: "取消订单" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh();
|
||||||
|
|
||||||
|
const timer = setInterval(() => {
|
||||||
|
refresh();
|
||||||
|
}, 3000);
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
clearInterval(timer);
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
option,
|
||||||
|
refresh,
|
||||||
|
};
|
||||||
|
}
|
||||||
49
src/views/dashboard1/hooks/useRightChart1.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import { onBeforeUnmount, ref } from "vue";
|
||||||
|
import { EChartsOption } from "echarts";
|
||||||
|
import { generateNumbers } from "@/utils";
|
||||||
|
|
||||||
|
export default function () {
|
||||||
|
const option = ref<EChartsOption>({});
|
||||||
|
|
||||||
|
function refresh() {
|
||||||
|
option.value = {
|
||||||
|
radar: {
|
||||||
|
indicator: [
|
||||||
|
{ name: "用户满意度", max: 200 },
|
||||||
|
{ name: "价格", max: 200 },
|
||||||
|
{ name: "品质", max: 200 },
|
||||||
|
{ name: "客户支持", max: 200 },
|
||||||
|
{ name: "可持续性", max: 200 },
|
||||||
|
{ name: "效果", max: 200 },
|
||||||
|
],
|
||||||
|
radius: 100,
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
type: "radar",
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
value: generateNumbers(1, 200, 6),
|
||||||
|
areaStyle: {},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh();
|
||||||
|
|
||||||
|
const timer = setInterval(() => {
|
||||||
|
refresh();
|
||||||
|
}, 3000);
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
clearInterval(timer);
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
option,
|
||||||
|
refresh,
|
||||||
|
};
|
||||||
|
}
|
||||||
97
src/views/dashboard1/hooks/useRightChart2.ts
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
import { onBeforeUnmount, ref } from "vue";
|
||||||
|
import { EChartsOption } from "echarts";
|
||||||
|
import { generateNumbers } from "@/utils";
|
||||||
|
|
||||||
|
export default function () {
|
||||||
|
const option = ref<EChartsOption>({});
|
||||||
|
|
||||||
|
function refresh() {
|
||||||
|
option.value = {
|
||||||
|
tooltip: {
|
||||||
|
trigger: "axis",
|
||||||
|
axisPointer: {
|
||||||
|
type: "shadow",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
top: "5%",
|
||||||
|
itemStyle: {
|
||||||
|
borderColor: "#fff",
|
||||||
|
borderWidth: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
left: "10%",
|
||||||
|
bottom: "8%",
|
||||||
|
containLabel: true,
|
||||||
|
},
|
||||||
|
xAxis: [
|
||||||
|
{
|
||||||
|
type: "value",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
yAxis: [
|
||||||
|
{
|
||||||
|
type: "category",
|
||||||
|
axisTick: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
data: ["一月", "二月", "三月", "四月", "五月", "六月", "七月"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: "预定",
|
||||||
|
type: "bar",
|
||||||
|
label: {
|
||||||
|
show: true,
|
||||||
|
position: "inside",
|
||||||
|
},
|
||||||
|
emphasis: {
|
||||||
|
focus: "series",
|
||||||
|
},
|
||||||
|
data: generateNumbers(1, 200, 7),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "购买",
|
||||||
|
type: "bar",
|
||||||
|
stack: "Total",
|
||||||
|
label: {
|
||||||
|
show: true,
|
||||||
|
},
|
||||||
|
emphasis: {
|
||||||
|
focus: "series",
|
||||||
|
},
|
||||||
|
data: generateNumbers(1, 300, 7),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "退货",
|
||||||
|
type: "bar",
|
||||||
|
stack: "Total",
|
||||||
|
label: {
|
||||||
|
show: true,
|
||||||
|
},
|
||||||
|
emphasis: {
|
||||||
|
focus: "series",
|
||||||
|
},
|
||||||
|
data: generateNumbers(-50, -10, 7),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh();
|
||||||
|
|
||||||
|
const timer = setInterval(() => {
|
||||||
|
refresh();
|
||||||
|
}, 3000);
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
clearInterval(timer);
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
option,
|
||||||
|
refresh,
|
||||||
|
};
|
||||||
|
}
|
||||||
59
src/views/dashboard1/hooks/useRightChart3.ts
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import { onBeforeUnmount, ref } from "vue";
|
||||||
|
import { EChartsOption } from "echarts";
|
||||||
|
import { generateNumbers } from "@/utils";
|
||||||
|
|
||||||
|
export default function () {
|
||||||
|
const option = ref<EChartsOption>({});
|
||||||
|
|
||||||
|
function refresh() {
|
||||||
|
option.value = {
|
||||||
|
xAxis: {},
|
||||||
|
yAxis: {},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
symbolSize: 20,
|
||||||
|
data: [
|
||||||
|
generateNumbers(1, 10, 2),
|
||||||
|
generateNumbers(1, 10, 2),
|
||||||
|
generateNumbers(1, 10, 2),
|
||||||
|
generateNumbers(1, 10, 2),
|
||||||
|
generateNumbers(1, 10, 2),
|
||||||
|
[8.07, 6.95],
|
||||||
|
[13.0, 7.58],
|
||||||
|
[14.0, 7.66],
|
||||||
|
[13.4, 6.81],
|
||||||
|
generateNumbers(1, 10, 2),
|
||||||
|
generateNumbers(1, 10, 2),
|
||||||
|
generateNumbers(1, 10, 2),
|
||||||
|
generateNumbers(1, 10, 2),
|
||||||
|
[11.5, 7.2],
|
||||||
|
[3.03, 4.23],
|
||||||
|
[1.05, 3.33],
|
||||||
|
[4.05, 4.96],
|
||||||
|
generateNumbers(1, 10, 2),
|
||||||
|
generateNumbers(1, 10, 2),
|
||||||
|
generateNumbers(1, 10, 2),
|
||||||
|
generateNumbers(1, 10, 2),
|
||||||
|
[5.02, 5.68],
|
||||||
|
],
|
||||||
|
type: "scatter",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh();
|
||||||
|
|
||||||
|
const timer = setInterval(() => {
|
||||||
|
refresh();
|
||||||
|
}, 3000);
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
clearInterval(timer);
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
option,
|
||||||
|
refresh,
|
||||||
|
};
|
||||||
|
}
|
||||||
2532
src/views/dashboard1/index.less
Normal file
89
src/views/dashboard1/index.vue
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, ref, type Component } from "vue";
|
||||||
|
import SwitchPageBtn from "./components/btn";
|
||||||
|
import backgroundVideo from "./assets/循环背景动画.mp4";
|
||||||
|
import AlarmManagementPage from "./pages/AlarmManagementPage.vue";
|
||||||
|
import DataReportPage from "./pages/DataReportPage.vue";
|
||||||
|
import EquipmentManagementPage from "./pages/EquipmentManagementPage.vue";
|
||||||
|
import IntegratedCenterPage from "./pages/IntegratedCenterPage.vue";
|
||||||
|
import PatrolPlanPage from "./pages/PatrolPlanPage.vue";
|
||||||
|
import VideoCenterPage from "./pages/VideoCenterPage.vue";
|
||||||
|
|
||||||
|
const currentTab = ref<number>(1);
|
||||||
|
|
||||||
|
const tabs = {
|
||||||
|
left: [
|
||||||
|
{ id: 1, text: "监管中心" },
|
||||||
|
{ id: 2, text: "设备管理" },
|
||||||
|
{ id: 3, text: "视频中心" },
|
||||||
|
],
|
||||||
|
right: [
|
||||||
|
{ id: 4, text: "告警管理" },
|
||||||
|
{ id: 5, text: "巡检记录" },
|
||||||
|
{ id: 6, text: "统计分析" },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const pageComponents: Record<number, Component> = {
|
||||||
|
1: IntegratedCenterPage,
|
||||||
|
2: EquipmentManagementPage,
|
||||||
|
3: VideoCenterPage,
|
||||||
|
4: AlarmManagementPage,
|
||||||
|
5: PatrolPlanPage,
|
||||||
|
6: DataReportPage,
|
||||||
|
};
|
||||||
|
|
||||||
|
const currentViewComponent = computed<Component>(
|
||||||
|
() => pageComponents[currentTab.value] ?? IntegratedCenterPage,
|
||||||
|
);
|
||||||
|
|
||||||
|
function switchPage(tab: number) {
|
||||||
|
currentTab.value = tab;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="dashboard">
|
||||||
|
<video
|
||||||
|
autoplay
|
||||||
|
class="background"
|
||||||
|
loop
|
||||||
|
muted
|
||||||
|
preload="auto"
|
||||||
|
:src="backgroundVideo"
|
||||||
|
></video>
|
||||||
|
<div class="main">
|
||||||
|
<div class="btn-group">
|
||||||
|
<div class="left">
|
||||||
|
<SwitchPageBtn
|
||||||
|
v-for="tab in tabs.left"
|
||||||
|
:key="tab.id"
|
||||||
|
:active="currentTab === tab.id"
|
||||||
|
:text="tab.text"
|
||||||
|
direction="left"
|
||||||
|
@click="switchPage(tab.id)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="right">
|
||||||
|
<SwitchPageBtn
|
||||||
|
v-for="tab in tabs.right"
|
||||||
|
:key="tab.id"
|
||||||
|
:active="currentTab === tab.id"
|
||||||
|
:text="tab.text"
|
||||||
|
direction="right"
|
||||||
|
@click="switchPage(tab.id)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="content-stage">
|
||||||
|
<KeepAlive>
|
||||||
|
<component :is="currentViewComponent" />
|
||||||
|
</KeepAlive>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="less">
|
||||||
|
@import "./index";
|
||||||
|
</style>
|
||||||
214
src/views/dashboard1/pages/AlarmManagementPage.vue
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, ref } from "vue";
|
||||||
|
|
||||||
|
interface AlarmRecord {
|
||||||
|
id: string;
|
||||||
|
type: string;
|
||||||
|
robot: string;
|
||||||
|
photo: string;
|
||||||
|
video: string;
|
||||||
|
status: "待研判" | "待处理" | "处置完成" | "误判";
|
||||||
|
time: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const alarms: AlarmRecord[] = [
|
||||||
|
{
|
||||||
|
id: "AL-001",
|
||||||
|
type: "线缆破损",
|
||||||
|
robot: "巡检机器人 R1",
|
||||||
|
photo: "破损抓拍_001.jpg",
|
||||||
|
video: "线缆破损_001.mp4",
|
||||||
|
status: "待研判",
|
||||||
|
time: "2026-06-23 10:24:18",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "AL-002",
|
||||||
|
type: "局部高温",
|
||||||
|
robot: "巡检机器人 Q1",
|
||||||
|
photo: "高温抓拍_017.jpg",
|
||||||
|
video: "局部高温_017.mp4",
|
||||||
|
status: "待处理",
|
||||||
|
time: "2026-06-23 10:18:42",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "AL-003",
|
||||||
|
type: "墙体裂缝",
|
||||||
|
robot: "巡检机器人 N3",
|
||||||
|
photo: "裂缝取证_006.jpg",
|
||||||
|
video: "墙体裂缝_006.mp4",
|
||||||
|
status: "处置完成",
|
||||||
|
time: "2026-06-23 09:56:11",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "AL-004",
|
||||||
|
type: "水渍渗漏",
|
||||||
|
robot: "巡检机器人 R2",
|
||||||
|
photo: "渗漏抓拍_012.jpg",
|
||||||
|
video: "水渍渗漏_012.mp4",
|
||||||
|
status: "误判",
|
||||||
|
time: "2026-06-23 09:20:36",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "AL-005",
|
||||||
|
type: "应急门开启异常",
|
||||||
|
robot: "巡检机器人 N1",
|
||||||
|
photo: "门禁取证_031.jpg",
|
||||||
|
video: "门禁异常_031.mp4",
|
||||||
|
status: "待处理",
|
||||||
|
time: "2026-06-23 08:42:57",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const keyword = ref("");
|
||||||
|
const pageSize = 4;
|
||||||
|
const currentPage = ref(1);
|
||||||
|
|
||||||
|
const filteredAlarms = computed(() => {
|
||||||
|
const search = keyword.value.trim().toLowerCase();
|
||||||
|
|
||||||
|
if (!search) {
|
||||||
|
return alarms;
|
||||||
|
}
|
||||||
|
|
||||||
|
return alarms.filter((alarm) =>
|
||||||
|
[alarm.id, alarm.type, alarm.robot, alarm.status]
|
||||||
|
.join(" ")
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(search),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const totalPages = computed(() =>
|
||||||
|
Math.max(1, Math.ceil(filteredAlarms.value.length / pageSize)),
|
||||||
|
);
|
||||||
|
|
||||||
|
const pagedAlarms = computed(() => {
|
||||||
|
const start = (currentPage.value - 1) * pageSize;
|
||||||
|
return filteredAlarms.value.slice(start, start + pageSize);
|
||||||
|
});
|
||||||
|
|
||||||
|
const visiblePages = computed(() => {
|
||||||
|
const pages: number[] = [];
|
||||||
|
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 = {
|
||||||
|
待研判: "alarm-status-pending",
|
||||||
|
待处理: "alarm-status-handling",
|
||||||
|
处置完成: "alarm-status-finished",
|
||||||
|
误判: "alarm-status-false",
|
||||||
|
};
|
||||||
|
|
||||||
|
return classMap[status];
|
||||||
|
}
|
||||||
|
|
||||||
|
function setPage(page: number) {
|
||||||
|
currentPage.value = page;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSearch() {
|
||||||
|
currentPage.value = 1;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="page-shell alarm-page">
|
||||||
|
<div class="page-grid management-layout">
|
||||||
|
<section class="page-card management-table-card">
|
||||||
|
<div class="management-toolbar">
|
||||||
|
<div class="toolbar-left">
|
||||||
|
<button class="page-action-btn primary" type="button">批量通知</button>
|
||||||
|
<button class="page-action-btn ghost" type="button">导出告警</button>
|
||||||
|
<input
|
||||||
|
v-model="keyword"
|
||||||
|
class="toolbar-input"
|
||||||
|
placeholder="搜索告警编号 / 告警类型 / 报警机器人"
|
||||||
|
type="text"
|
||||||
|
@input="handleSearch"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="toolbar-right">共 {{ filteredAlarms.length }} 条告警记录</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="table-shell">
|
||||||
|
<table class="device-table alarm-device-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>告警编号</th>
|
||||||
|
<th>告警类型</th>
|
||||||
|
<th>报警机器人</th>
|
||||||
|
<th>取证照片</th>
|
||||||
|
<th>取证视频</th>
|
||||||
|
<th>案件状态</th>
|
||||||
|
<th>告警时间</th>
|
||||||
|
<th>操作</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="alarm in pagedAlarms" :key="alarm.id">
|
||||||
|
<td>{{ alarm.id }}</td>
|
||||||
|
<td>{{ alarm.type }}</td>
|
||||||
|
<td>{{ alarm.robot }}</td>
|
||||||
|
<td>
|
||||||
|
<div class="evidence-chip photo">{{ alarm.photo }}</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="evidence-chip video">{{ alarm.video }}</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span :class="['table-status', getAlarmStatusClass(alarm.status)]">
|
||||||
|
{{ alarm.status }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>{{ alarm.time }}</td>
|
||||||
|
<td>
|
||||||
|
<div class="table-actions">
|
||||||
|
<button type="button">研判</button>
|
||||||
|
<button type="button">通知</button>
|
||||||
|
<button class="danger" type="button">删除</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pagination-bar">
|
||||||
|
<button
|
||||||
|
class="pagination-btn"
|
||||||
|
:disabled="currentPage === 1"
|
||||||
|
type="button"
|
||||||
|
@click="setPage(currentPage - 1)"
|
||||||
|
>
|
||||||
|
上一页
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-for="page in visiblePages"
|
||||||
|
:key="page"
|
||||||
|
:class="['pagination-btn', { active: currentPage === page }]"
|
||||||
|
type="button"
|
||||||
|
@click="setPage(page)"
|
||||||
|
>
|
||||||
|
{{ page }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="pagination-btn"
|
||||||
|
:disabled="currentPage === totalPages"
|
||||||
|
type="button"
|
||||||
|
@click="setPage(currentPage + 1)"
|
||||||
|
>
|
||||||
|
下一页
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
492
src/views/dashboard1/pages/DataReportPage.vue
Normal file
@@ -0,0 +1,492 @@
|
|||||||
|
<script lang="ts" 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[] = [
|
||||||
|
{ 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" },
|
||||||
|
{ 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-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-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-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-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-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-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" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const trendData: TrendRecord[] = [
|
||||||
|
{ date: "06-09", project: "西江隧道", total: 1, closed: 1 },
|
||||||
|
{ date: "06-09", project: "青云隧道", total: 1, closed: 0 },
|
||||||
|
{ date: "06-09", project: "南山隧道", total: 1, closed: 1 },
|
||||||
|
{ date: "06-10", project: "西江隧道", total: 2, closed: 1 },
|
||||||
|
{ date: "06-10", project: "青云隧道", total: 1, closed: 0 },
|
||||||
|
{ date: "06-10", project: "南山隧道", total: 1, closed: 1 },
|
||||||
|
{ date: "06-11", project: "西江隧道", total: 2, closed: 1 },
|
||||||
|
{ date: "06-11", project: "青云隧道", total: 1, closed: 1 },
|
||||||
|
{ date: "06-11", project: "南山隧道", total: 2, closed: 1 },
|
||||||
|
{ date: "06-12", project: "西江隧道", total: 1, closed: 1 },
|
||||||
|
{ date: "06-12", project: "青云隧道", total: 1, closed: 1 },
|
||||||
|
{ date: "06-12", project: "南山隧道", total: 2, closed: 1 },
|
||||||
|
{ date: "06-13", project: "西江隧道", total: 2, closed: 1 },
|
||||||
|
{ date: "06-13", project: "青云隧道", total: 2, closed: 1 },
|
||||||
|
{ date: "06-13", project: "南山隧道", total: 2, closed: 2 },
|
||||||
|
{ date: "06-14", project: "西江隧道", total: 2, closed: 1 },
|
||||||
|
{ date: "06-14", project: "青云隧道", total: 1, closed: 1 },
|
||||||
|
{ date: "06-14", project: "南山隧道", total: 2, closed: 2 },
|
||||||
|
{ date: "06-15", project: "西江隧道", total: 3, closed: 2 },
|
||||||
|
{ date: "06-15", project: "青云隧道", total: 2, closed: 1 },
|
||||||
|
{ date: "06-15", project: "南山隧道", total: 2, closed: 2 },
|
||||||
|
{ date: "06-16", project: "西江隧道", total: 2, closed: 1 },
|
||||||
|
{ date: "06-16", project: "青云隧道", total: 2, closed: 1 },
|
||||||
|
{ date: "06-16", project: "南山隧道", total: 2, closed: 2 },
|
||||||
|
{ date: "06-17", project: "西江隧道", total: 3, closed: 2 },
|
||||||
|
{ date: "06-17", project: "青云隧道", total: 2, closed: 1 },
|
||||||
|
{ date: "06-17", project: "南山隧道", total: 3, closed: 3 },
|
||||||
|
{ date: "06-18", project: "西江隧道", total: 2, closed: 2 },
|
||||||
|
{ date: "06-18", project: "青云隧道", total: 2, closed: 1 },
|
||||||
|
{ date: "06-18", project: "南山隧道", total: 3, closed: 2 },
|
||||||
|
{ date: "06-19", project: "西江隧道", total: 3, closed: 2 },
|
||||||
|
{ date: "06-19", project: "青云隧道", total: 2, closed: 2 },
|
||||||
|
{ date: "06-19", project: "南山隧道", total: 4, closed: 3 },
|
||||||
|
{ date: "06-20", project: "西江隧道", total: 3, closed: 2 },
|
||||||
|
{ date: "06-20", project: "青云隧道", total: 2, closed: 2 },
|
||||||
|
{ date: "06-20", project: "南山隧道", total: 3, closed: 2 },
|
||||||
|
{ date: "06-21", project: "西江隧道", total: 2, closed: 2 },
|
||||||
|
{ date: "06-21", project: "青云隧道", total: 2, closed: 1 },
|
||||||
|
{ date: "06-21", project: "南山隧道", total: 3, closed: 2 },
|
||||||
|
{ date: "06-22", project: "西江隧道", total: 4, closed: 3 },
|
||||||
|
{ date: "06-22", project: "青云隧道", total: 2, closed: 1 },
|
||||||
|
{ 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 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const trendSeries = computed(() => {
|
||||||
|
const grouped = trendData.reduce<Record<string, { total: number; closed: number }>>(
|
||||||
|
(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,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
const trendOption = computed<EChartsOption>(() => {
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
trigger: "axis",
|
||||||
|
backgroundColor: "rgba(6, 20, 42, 0.92)",
|
||||||
|
borderColor: "rgba(96, 208, 255, 0.24)",
|
||||||
|
textStyle: {
|
||||||
|
color: "#eefcff",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
xAxis: {
|
||||||
|
type: "category",
|
||||||
|
boundaryGap: false,
|
||||||
|
data: dates,
|
||||||
|
axisLine: {
|
||||||
|
lineStyle: {
|
||||||
|
color: "rgba(96, 188, 242, 0.2)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
axisLabel: {
|
||||||
|
color: "rgba(181, 230, 249, 0.74)",
|
||||||
|
fontSize: 11,
|
||||||
|
},
|
||||||
|
axisTick: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
type: "value",
|
||||||
|
splitNumber: 5,
|
||||||
|
axisLine: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
axisTick: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
axisLabel: {
|
||||||
|
color: "rgba(181, 230, 249, 0.62)",
|
||||||
|
fontSize: 11,
|
||||||
|
},
|
||||||
|
splitLine: {
|
||||||
|
lineStyle: {
|
||||||
|
color: "rgba(97, 189, 242, 0.12)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: "新增告警",
|
||||||
|
type: "line",
|
||||||
|
smooth: true,
|
||||||
|
symbol: "circle",
|
||||||
|
symbolSize: 7,
|
||||||
|
data: totalValues,
|
||||||
|
lineStyle: {
|
||||||
|
width: 3,
|
||||||
|
color: "#74ecff",
|
||||||
|
},
|
||||||
|
itemStyle: {
|
||||||
|
color: "#74ecff",
|
||||||
|
borderColor: "#081f3e",
|
||||||
|
borderWidth: 2,
|
||||||
|
},
|
||||||
|
areaStyle: {
|
||||||
|
color: "rgba(116, 236, 255, 0.12)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "闭环告警",
|
||||||
|
type: "line",
|
||||||
|
smooth: true,
|
||||||
|
symbol: "circle",
|
||||||
|
symbolSize: 7,
|
||||||
|
data: closedValues,
|
||||||
|
lineStyle: {
|
||||||
|
width: 3,
|
||||||
|
color: "#62e0a8",
|
||||||
|
},
|
||||||
|
itemStyle: {
|
||||||
|
color: "#62e0a8",
|
||||||
|
borderColor: "#081f3e",
|
||||||
|
borderWidth: 2,
|
||||||
|
},
|
||||||
|
areaStyle: {
|
||||||
|
color: "rgba(98, 224, 168, 0.08)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const typeStats = computed(() => {
|
||||||
|
const statsMap = new Map<string, number>();
|
||||||
|
reportData.forEach((item) => {
|
||||||
|
statsMap.set(item.type, (statsMap.get(item.type) ?? 0) + 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
return Array.from(statsMap.entries()).map(([label, value]) => ({
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
const typeRadarOption = computed<EChartsOption>(() => {
|
||||||
|
const maxValue = Math.max(...typeStats.value.map((item) => item.value), 1);
|
||||||
|
const indicators = typeStats.value.map((item) => ({
|
||||||
|
name: item.label,
|
||||||
|
max: maxValue,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
tooltip: {
|
||||||
|
trigger: "item",
|
||||||
|
backgroundColor: "rgba(6, 20, 42, 0.92)",
|
||||||
|
borderColor: "rgba(96, 208, 255, 0.24)",
|
||||||
|
textStyle: {
|
||||||
|
color: "#eefcff",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
radar: {
|
||||||
|
radius: "68%",
|
||||||
|
center: ["50%", "54%"],
|
||||||
|
indicator: indicators,
|
||||||
|
splitNumber: 4,
|
||||||
|
axisName: {
|
||||||
|
color: "#dff7ff",
|
||||||
|
fontSize: 12,
|
||||||
|
},
|
||||||
|
axisLine: {
|
||||||
|
lineStyle: {
|
||||||
|
color: "rgba(97, 189, 242, 0.22)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
splitLine: {
|
||||||
|
lineStyle: {
|
||||||
|
color: "rgba(97, 189, 242, 0.16)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
splitArea: {
|
||||||
|
areaStyle: {
|
||||||
|
color: ["rgba(16, 52, 94, 0.10)", "rgba(16, 52, 94, 0.05)"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
type: "radar",
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
value: typeStats.value.map((item) => item.value),
|
||||||
|
name: "告警类型分布",
|
||||||
|
areaStyle: {
|
||||||
|
color: "rgba(73, 207, 255, 0.20)",
|
||||||
|
},
|
||||||
|
lineStyle: {
|
||||||
|
color: "#61dcff",
|
||||||
|
width: 2.5,
|
||||||
|
},
|
||||||
|
itemStyle: {
|
||||||
|
color: "#8ff3ff",
|
||||||
|
borderColor: "#07203e",
|
||||||
|
borderWidth: 2,
|
||||||
|
},
|
||||||
|
symbolSize: 8,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const statusStats = computed(() => {
|
||||||
|
const order: AlarmStatus[] = ["待研判", "待处理", "处置完成", "误判"];
|
||||||
|
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)}%`,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const statusPieOption = computed<EChartsOption>(() => ({
|
||||||
|
tooltip: {
|
||||||
|
trigger: "item",
|
||||||
|
backgroundColor: "rgba(6, 20, 42, 0.92)",
|
||||||
|
borderColor: "rgba(96, 208, 255, 0.24)",
|
||||||
|
textStyle: {
|
||||||
|
color: "#eefcff",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
type: "pie",
|
||||||
|
radius: ["46%", "72%"],
|
||||||
|
center: ["50%", "50%"],
|
||||||
|
avoidLabelOverlap: false,
|
||||||
|
itemStyle: {
|
||||||
|
borderColor: "#081f3e",
|
||||||
|
borderWidth: 3,
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
show: true,
|
||||||
|
color: "#dff7ff",
|
||||||
|
formatter: "{b}\n{d}%",
|
||||||
|
fontSize: 12,
|
||||||
|
},
|
||||||
|
labelLine: {
|
||||||
|
lineStyle: {
|
||||||
|
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",
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}));
|
||||||
|
|
||||||
|
const projectDistribution = computed(() => {
|
||||||
|
const now = new Date("2026-06-23T23:59:59");
|
||||||
|
const rangeDaysMap: Record<DistributionRange, number> = {
|
||||||
|
近一周: 7,
|
||||||
|
近一个月: 30,
|
||||||
|
近三个月: 90,
|
||||||
|
};
|
||||||
|
const threshold = new Date(
|
||||||
|
now.getTime() - rangeDaysMap[distributionRange.value] * 24 * 60 * 60 * 1000,
|
||||||
|
);
|
||||||
|
const data = reportData.filter((item) => new Date(item.time.replace(" ", "T")) >= threshold);
|
||||||
|
const statsMap = new Map<string, number>();
|
||||||
|
|
||||||
|
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]) => ({
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
width: `${(value / maxValue) * 100}%`,
|
||||||
|
}))
|
||||||
|
.sort((a, b) => b.value - a.value);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="page-shell report-page report-dashboard-page">
|
||||||
|
<div class="report-dashboard">
|
||||||
|
<section class="report-main-grid">
|
||||||
|
<div class="page-card report-panel trend-panel">
|
||||||
|
<div class="report-panel-header">
|
||||||
|
<div class="page-card-title">告警趋势分析</div>
|
||||||
|
</div>
|
||||||
|
<div class="trend-chart">
|
||||||
|
<Chart :option="trendOption" />
|
||||||
|
</div>
|
||||||
|
<div class="trend-legend">
|
||||||
|
<span><i class="legend-dot total"></i>新增告警</span>
|
||||||
|
<span><i class="legend-dot closed"></i>闭环告警</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="page-card report-panel type-panel">
|
||||||
|
<div class="report-panel-header">
|
||||||
|
<div class="page-card-title">告警类型分布</div>
|
||||||
|
</div>
|
||||||
|
<div class="type-radar-layout">
|
||||||
|
<div class="type-radar-chart">
|
||||||
|
<Chart :option="typeRadarOption" />
|
||||||
|
</div>
|
||||||
|
<div class="type-radar-legend">
|
||||||
|
<div v-for="item in typeStats" :key="item.label" class="type-legend-item">
|
||||||
|
<span class="type-legend-name">{{ item.label }}</span>
|
||||||
|
<span class="type-legend-value">{{ item.value }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="page-card report-panel status-panel">
|
||||||
|
<div class="report-panel-header">
|
||||||
|
<div class="page-card-title">案件状态分布</div>
|
||||||
|
</div>
|
||||||
|
<div class="status-pie-layout">
|
||||||
|
<div class="status-pie-chart">
|
||||||
|
<Chart :option="statusPieOption" />
|
||||||
|
</div>
|
||||||
|
<div class="status-pie-legend">
|
||||||
|
<div v-for="item in statusStats" :key="item.label" class="status-legend-item">
|
||||||
|
<span
|
||||||
|
:class="[
|
||||||
|
'status-legend-dot',
|
||||||
|
item.label === '待研判'
|
||||||
|
? 'pending'
|
||||||
|
: item.label === '待处理'
|
||||||
|
? 'handling'
|
||||||
|
: item.label === '处置完成'
|
||||||
|
? 'finished'
|
||||||
|
: 'false',
|
||||||
|
]"
|
||||||
|
></span>
|
||||||
|
<span class="status-legend-name">{{ item.label }}</span>
|
||||||
|
<span class="status-legend-value">{{ item.value }}</span>
|
||||||
|
<span class="status-legend-percent">{{ item.percent }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="page-card report-panel ranking-panel">
|
||||||
|
<div class="report-panel-header">
|
||||||
|
<div class="page-card-title">项目告警分布</div>
|
||||||
|
<div class="report-filter-tabs">
|
||||||
|
<button
|
||||||
|
v-for="range in ['近一周', '近一个月', '近三个月']"
|
||||||
|
:key="range"
|
||||||
|
:class="['report-filter-tab', { active: distributionRange === range }]"
|
||||||
|
type="button"
|
||||||
|
@click="distributionRange = range as DistributionRange"
|
||||||
|
>
|
||||||
|
{{ range }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="report-bar-list">
|
||||||
|
<div v-for="item in projectDistribution" :key="item.label" class="report-bar-row">
|
||||||
|
<div class="bar-row-head">
|
||||||
|
<span>{{ item.label }}</span>
|
||||||
|
<span>{{ item.value }} 次</span>
|
||||||
|
</div>
|
||||||
|
<div class="report-bar-track">
|
||||||
|
<span class="report-bar-fill blue" :style="{ width: item.width }"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
2299
src/views/dashboard1/pages/EquipmentManagementPage.vue
Normal file
484
src/views/dashboard1/pages/IntegratedCenterPage.vue
Normal file
@@ -0,0 +1,484 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, ref } from "vue";
|
||||||
|
import monitorVideo from "../assets/循环背景动画.mp4";
|
||||||
|
|
||||||
|
interface DeviceItem {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
status: "空闲" | "离线" | "充电中";
|
||||||
|
location: string;
|
||||||
|
battery: string;
|
||||||
|
task: string;
|
||||||
|
linearSpeed: string;
|
||||||
|
angularSpeed: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ProjectItem {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
devices: DeviceItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NavPoint {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NavScenario {
|
||||||
|
routePath: string;
|
||||||
|
cloudPoints: NavPoint[];
|
||||||
|
checkPoints: Array<{ x: number; y: number; label: string }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AlertItem {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
level: "高" | "中" | "低";
|
||||||
|
location: string;
|
||||||
|
time: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const projectDevices: ProjectItem[] = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: "西江隧道",
|
||||||
|
devices: [
|
||||||
|
{
|
||||||
|
id: "ROBOT-001",
|
||||||
|
name: "巡检机器人 R1",
|
||||||
|
status: "空闲",
|
||||||
|
location: "东洞口至 K1+240",
|
||||||
|
battery: "82%",
|
||||||
|
task: "日常巡检",
|
||||||
|
linearSpeed: "0.42m/s",
|
||||||
|
angularSpeed: "0.08rad/s",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "ROBOT-002",
|
||||||
|
name: "巡检机器人 R2",
|
||||||
|
status: "空闲",
|
||||||
|
location: "K1+240 至 K2+120",
|
||||||
|
battery: "76%",
|
||||||
|
task: "设备复核",
|
||||||
|
linearSpeed: "0.38m/s",
|
||||||
|
angularSpeed: "0.06rad/s",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "ROBOT-003",
|
||||||
|
name: "巡检机器人 R3",
|
||||||
|
status: "充电中",
|
||||||
|
location: "中控区",
|
||||||
|
battery: "48%",
|
||||||
|
task: "回库维护",
|
||||||
|
linearSpeed: "0.00m/s",
|
||||||
|
angularSpeed: "0.00rad/s",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "ROBOT-004",
|
||||||
|
name: "巡检机器人 R4",
|
||||||
|
status: "空闲",
|
||||||
|
location: "K2+120 至西洞口",
|
||||||
|
battery: "91%",
|
||||||
|
task: "夜间待命",
|
||||||
|
linearSpeed: "0.31m/s",
|
||||||
|
angularSpeed: "0.04rad/s",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const recentAlerts: AlertItem[] = [
|
||||||
|
{
|
||||||
|
id: "AL-001",
|
||||||
|
title: "线缆破损",
|
||||||
|
level: "高",
|
||||||
|
location: "西江隧道 K1+240",
|
||||||
|
time: "10:24:18",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "AL-002",
|
||||||
|
title: "局部高温",
|
||||||
|
level: "中",
|
||||||
|
location: "西江隧道 泵房区",
|
||||||
|
time: "10:18:42",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "AL-003",
|
||||||
|
title: "墙体裂缝",
|
||||||
|
level: "低",
|
||||||
|
location: "西江隧道 K1+860",
|
||||||
|
time: "09:56:11",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const alertTabs = ["全部", "线缆破损", "局部高温", "墙体裂缝"] as const;
|
||||||
|
const activeAlertTab = ref<(typeof alertTabs)[number]>("全部");
|
||||||
|
const activeCameraMode = ref<"white" | "infrared">("white");
|
||||||
|
const controlMode = ref<"人工接管" | "自主巡检">("人工接管");
|
||||||
|
const targetLiftHeight = ref("1200");
|
||||||
|
const liftRealtimeStatus = ref("待命");
|
||||||
|
const liftRealtimeHeight = ref("1180");
|
||||||
|
|
||||||
|
const navScenario: NavScenario = {
|
||||||
|
routePath:
|
||||||
|
"M 72 310 C 118 282, 164 265, 212 248 S 312 205, 360 216 S 446 270, 504 248 S 612 188, 690 210",
|
||||||
|
cloudPoints: [
|
||||||
|
{ x: 52, y: 120 },
|
||||||
|
{ x: 86, y: 132 },
|
||||||
|
{ x: 128, y: 148 },
|
||||||
|
{ x: 164, y: 138 },
|
||||||
|
{ x: 204, y: 118 },
|
||||||
|
{ x: 252, y: 104 },
|
||||||
|
{ x: 304, y: 120 },
|
||||||
|
{ x: 346, y: 142 },
|
||||||
|
{ x: 392, y: 168 },
|
||||||
|
{ x: 442, y: 162 },
|
||||||
|
{ x: 494, y: 134 },
|
||||||
|
{ x: 548, y: 116 },
|
||||||
|
{ x: 602, y: 132 },
|
||||||
|
{ x: 660, y: 152 },
|
||||||
|
{ x: 94, y: 254 },
|
||||||
|
{ x: 150, y: 236 },
|
||||||
|
{ x: 214, y: 220 },
|
||||||
|
{ x: 278, y: 208 },
|
||||||
|
{ x: 338, y: 214 },
|
||||||
|
{ x: 400, y: 236 },
|
||||||
|
{ x: 468, y: 258 },
|
||||||
|
{ x: 530, y: 238 },
|
||||||
|
{ x: 592, y: 214 },
|
||||||
|
{ x: 656, y: 226 },
|
||||||
|
],
|
||||||
|
checkPoints: [
|
||||||
|
{ x: 104, y: 280, label: "起点" },
|
||||||
|
{ x: 268, y: 224, label: "高温检测点" },
|
||||||
|
{ x: 480, y: 256, label: "泵房区" },
|
||||||
|
{ x: 674, y: 214, label: "终点" },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const currentProject = computed(() => projectDevices[0]);
|
||||||
|
const currentDevices = computed(() => currentProject.value.devices);
|
||||||
|
const currentRobot = computed(() => currentDevices.value[0]);
|
||||||
|
const currentScenario = computed(() => navScenario);
|
||||||
|
|
||||||
|
const filteredRecentAlerts = computed(() => {
|
||||||
|
if (activeAlertTab.value === "全部") {
|
||||||
|
return recentAlerts;
|
||||||
|
}
|
||||||
|
|
||||||
|
return recentAlerts.filter((alert) => alert.title === activeAlertTab.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
function switchAlertTab(tab: (typeof alertTabs)[number]) {
|
||||||
|
activeAlertTab.value = tab;
|
||||||
|
}
|
||||||
|
|
||||||
|
function switchCameraMode(mode: "white" | "infrared") {
|
||||||
|
activeCameraMode.value = mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleControlMode() {
|
||||||
|
controlMode.value =
|
||||||
|
controlMode.value === "人工接管" ? "自主巡检" : "人工接管";
|
||||||
|
}
|
||||||
|
|
||||||
|
function runToTargetHeight() {
|
||||||
|
const normalizedHeight = targetLiftHeight.value.replace(/[^\d]/g, "");
|
||||||
|
|
||||||
|
targetLiftHeight.value = normalizedHeight || "0";
|
||||||
|
liftRealtimeStatus.value = "运行中";
|
||||||
|
liftRealtimeHeight.value = targetLiftHeight.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStatusClass(status: DeviceItem["status"]) {
|
||||||
|
const classMap: Record<DeviceItem["status"], string> = {
|
||||||
|
空闲: "status-idle",
|
||||||
|
离线: "status-offline",
|
||||||
|
充电中: "status-charging",
|
||||||
|
};
|
||||||
|
|
||||||
|
return classMap[status];
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="center">
|
||||||
|
<div class="left">
|
||||||
|
<div class="card-1 device-card">
|
||||||
|
<div class="title">设备列表</div>
|
||||||
|
<div class="device-panel">
|
||||||
|
<div class="project-name">{{ currentProject.name }}</div>
|
||||||
|
<div class="device-list">
|
||||||
|
<div v-for="device in currentDevices" :key="device.id" class="device-item">
|
||||||
|
<div class="device-main">
|
||||||
|
<div class="device-name">{{ device.name }}</div>
|
||||||
|
<div :class="['device-status', getStatusClass(device.status)]">
|
||||||
|
{{ device.status }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="device-meta">
|
||||||
|
<span>{{ device.id }}</span>
|
||||||
|
<span>{{ device.location }}</span>
|
||||||
|
<span>{{ device.battery }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-2 alert-card">
|
||||||
|
<div class="title">近期告警</div>
|
||||||
|
<div class="alert-panel">
|
||||||
|
<div class="alert-tabs">
|
||||||
|
<button
|
||||||
|
v-for="tab in alertTabs"
|
||||||
|
:key="tab"
|
||||||
|
:class="['alert-tab', { active: activeAlertTab === tab }]"
|
||||||
|
type="button"
|
||||||
|
@click="switchAlertTab(tab)"
|
||||||
|
>
|
||||||
|
{{ tab }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<template v-if="filteredRecentAlerts.length">
|
||||||
|
<div v-for="alert in filteredRecentAlerts" :key="alert.id" class="alert-item">
|
||||||
|
<div class="alert-main">
|
||||||
|
<div class="alert-title">{{ alert.title }}</div>
|
||||||
|
<div class="alert-level">{{ alert.level }}级</div>
|
||||||
|
</div>
|
||||||
|
<div class="alert-meta">
|
||||||
|
<span>{{ alert.id }}</span>
|
||||||
|
<span>{{ alert.location }}</span>
|
||||||
|
<span>{{ alert.time }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div v-else class="alert-empty">当前筛选下暂无告警</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="middle">
|
||||||
|
<div class="nav-map-panel">
|
||||||
|
<div class="nav-map-stage">
|
||||||
|
<svg class="nav-map-svg" viewBox="0 0 760 420" preserveAspectRatio="xMidYMid meet">
|
||||||
|
<g class="grid-layer">
|
||||||
|
<line v-for="line in 7" :key="`h-${line}`" :x1="40" :y1="line * 52" :x2="720" :y2="line * 52" />
|
||||||
|
<line v-for="line in 12" :key="`v-${line}`" :x1="line * 56" :y1="40" :x2="line * 56" :y2="380" />
|
||||||
|
</g>
|
||||||
|
<g class="cloud-layer">
|
||||||
|
<circle
|
||||||
|
v-for="(point, index) in currentScenario.cloudPoints"
|
||||||
|
:key="`point-${index}`"
|
||||||
|
:cx="point.x"
|
||||||
|
:cy="point.y"
|
||||||
|
:r="index % 3 === 0 ? 3 : 2"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
<path class="route-glow" :d="currentScenario.routePath" />
|
||||||
|
<path class="route-line" :d="currentScenario.routePath" />
|
||||||
|
<g class="checkpoint-layer">
|
||||||
|
<g
|
||||||
|
v-for="point in currentScenario.checkPoints"
|
||||||
|
:key="point.label"
|
||||||
|
class="checkpoint"
|
||||||
|
:transform="`translate(${point.x}, ${point.y})`"
|
||||||
|
>
|
||||||
|
<circle r="7"></circle>
|
||||||
|
<text x="12" y="4">{{ point.label }}</text>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g class="robot-layer">
|
||||||
|
<circle class="robot-shadow" r="18">
|
||||||
|
<animateMotion dur="18s" repeatCount="indefinite" :path="currentScenario.routePath" />
|
||||||
|
</circle>
|
||||||
|
<circle class="robot-body" r="10">
|
||||||
|
<animateMotion dur="18s" repeatCount="indefinite" rotate="auto" :path="currentScenario.routePath" />
|
||||||
|
</circle>
|
||||||
|
<path class="robot-head" d="M -3 -9 L 8 0 L -3 9 Z">
|
||||||
|
<animateMotion dur="18s" repeatCount="indefinite" rotate="auto" :path="currentScenario.routePath" />
|
||||||
|
</path>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
<div class="nav-map-overlay top-left">激光雷达点云</div>
|
||||||
|
<div class="nav-map-overlay top-right">路径跟踪中</div>
|
||||||
|
<div class="nav-map-overlay bottom-left">
|
||||||
|
机器人编号:{{ currentRobot.id }} / {{ currentRobot.task }}
|
||||||
|
</div>
|
||||||
|
<div class="nav-map-overlay bottom-right">
|
||||||
|
线速度 {{ currentRobot.linearSpeed }} / 角速度 {{ currentRobot.angularSpeed }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="nav-map-footer">
|
||||||
|
<div class="footer-item">
|
||||||
|
<span class="footer-label">当前位置</span>
|
||||||
|
<span class="footer-value">{{ currentRobot.location }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="footer-item">
|
||||||
|
<span class="footer-label">电量</span>
|
||||||
|
<span class="footer-value">{{ currentRobot.battery }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="footer-item">
|
||||||
|
<span class="footer-label">任务状态</span>
|
||||||
|
<span class="footer-value">{{ currentRobot.task }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="footer-item">
|
||||||
|
<span class="footer-label">实时线速度</span>
|
||||||
|
<span class="footer-value">{{ currentRobot.linearSpeed }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="footer-item">
|
||||||
|
<span class="footer-label">实时角速度</span>
|
||||||
|
<span class="footer-value">{{ currentRobot.angularSpeed }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="right">
|
||||||
|
<div class="card-1 video-card">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="title">视频监控</div>
|
||||||
|
<div class="camera-switch header-switch">
|
||||||
|
<button
|
||||||
|
:class="['camera-btn', { active: activeCameraMode === 'white' }]"
|
||||||
|
type="button"
|
||||||
|
@click="switchCameraMode('white')"
|
||||||
|
>
|
||||||
|
白光
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
:class="['camera-btn', { active: activeCameraMode === 'infrared' }]"
|
||||||
|
type="button"
|
||||||
|
@click="switchCameraMode('infrared')"
|
||||||
|
>
|
||||||
|
红外
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="video-panel">
|
||||||
|
<div class="video-screen">
|
||||||
|
<video
|
||||||
|
:class="['monitor-video', { infrared: activeCameraMode === 'infrared' }]"
|
||||||
|
autoplay
|
||||||
|
loop
|
||||||
|
muted
|
||||||
|
playsinline
|
||||||
|
:src="monitorVideo"
|
||||||
|
></video>
|
||||||
|
<div class="scan-line"></div>
|
||||||
|
<div class="video-overlay top-left">实时视频流</div>
|
||||||
|
<div class="video-overlay top-right">
|
||||||
|
{{ activeCameraMode === "white" ? "白光镜头" : "红外镜头" }}
|
||||||
|
</div>
|
||||||
|
<div class="video-overlay bottom-left">{{ currentRobot.id }}</div>
|
||||||
|
<div class="video-overlay bottom-right">{{ currentRobot.location }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-2 control-card">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="title">远程控制</div>
|
||||||
|
<button class="mode-toggle-btn" type="button" @click="toggleControlMode">
|
||||||
|
<span :class="['mode-toggle-chip', { active: controlMode === '人工接管' }]">
|
||||||
|
人工接管
|
||||||
|
</span>
|
||||||
|
<span :class="['mode-toggle-chip', { active: controlMode === '自主巡检' }]">
|
||||||
|
自主巡检
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="control-panel">
|
||||||
|
<div class="control-status">
|
||||||
|
<div class="status-item">
|
||||||
|
<span class="status-label">控制对象</span>
|
||||||
|
<span class="status-value">{{ currentRobot.id }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="status-item">
|
||||||
|
<span class="status-label">当前模式</span>
|
||||||
|
<span class="status-value">{{ controlMode }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="control-main">
|
||||||
|
<div class="control-top-row">
|
||||||
|
<div class="vehicle-section">
|
||||||
|
<div class="section-title control-block-title">底盘驱动</div>
|
||||||
|
<div class="vehicle-pad">
|
||||||
|
<button class="drive-btn drive-up" type="button">
|
||||||
|
<span class="drive-arrow">↑</span>
|
||||||
|
<span class="drive-label">前进</span>
|
||||||
|
</button>
|
||||||
|
<button class="drive-btn drive-left" type="button">
|
||||||
|
<span class="drive-arrow">←</span>
|
||||||
|
<span class="drive-label">左转</span>
|
||||||
|
</button>
|
||||||
|
<div class="drive-core" aria-hidden="true"></div>
|
||||||
|
<button class="drive-btn drive-right" type="button">
|
||||||
|
<span class="drive-arrow">→</span>
|
||||||
|
<span class="drive-label">右转</span>
|
||||||
|
</button>
|
||||||
|
<button class="drive-btn drive-down" type="button">
|
||||||
|
<span class="drive-arrow">↓</span>
|
||||||
|
<span class="drive-label">后退</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="control-section gimbal-section">
|
||||||
|
<div class="section-title">云台控制</div>
|
||||||
|
<div class="vehicle-pad gimbal-wheel">
|
||||||
|
<button class="drive-btn drive-up" type="button">
|
||||||
|
<span class="drive-arrow">↑</span>
|
||||||
|
<span class="drive-label">上</span>
|
||||||
|
</button>
|
||||||
|
<button class="drive-btn drive-left" type="button">
|
||||||
|
<span class="drive-arrow">←</span>
|
||||||
|
<span class="drive-label">左</span>
|
||||||
|
</button>
|
||||||
|
<div class="drive-core" aria-hidden="true"></div>
|
||||||
|
<button class="drive-btn drive-right" type="button">
|
||||||
|
<span class="drive-arrow">→</span>
|
||||||
|
<span class="drive-label">右</span>
|
||||||
|
</button>
|
||||||
|
<button class="drive-btn drive-down" type="button">
|
||||||
|
<span class="drive-arrow">↓</span>
|
||||||
|
<span class="drive-label">下</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="control-section lift-section">
|
||||||
|
<div class="section-title">升降柱控制</div>
|
||||||
|
<div class="lift-control-panel">
|
||||||
|
<label class="lift-input-group">
|
||||||
|
<span class="lift-input-label">目标高度</span>
|
||||||
|
<div class="lift-input-shell">
|
||||||
|
<input
|
||||||
|
v-model="targetLiftHeight"
|
||||||
|
class="lift-input"
|
||||||
|
type="text"
|
||||||
|
inputmode="numeric"
|
||||||
|
/>
|
||||||
|
<span class="lift-unit">mm</span>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
<button class="lift-submit-btn" type="button" @click="runToTargetHeight">
|
||||||
|
运行到指定高度
|
||||||
|
</button>
|
||||||
|
<div class="lift-status-panel">
|
||||||
|
<div class="lift-status-item">
|
||||||
|
<span class="lift-status-label">实时状态</span>
|
||||||
|
<span class="lift-status-value">{{ liftRealtimeStatus }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="lift-status-item">
|
||||||
|
<span class="lift-status-label">当前高度</span>
|
||||||
|
<span class="lift-status-value">{{ liftRealtimeHeight }} mm</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
161
src/views/dashboard1/pages/PatrolPlanPage.vue
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref } from "vue";
|
||||||
|
|
||||||
|
interface PatrolAlarmRecord {
|
||||||
|
id: string;
|
||||||
|
type: string;
|
||||||
|
reportTime: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PatrolRecord {
|
||||||
|
id: string;
|
||||||
|
robot: string;
|
||||||
|
startTime: string;
|
||||||
|
endTime: string;
|
||||||
|
mileage: string;
|
||||||
|
alarms: PatrolAlarmRecord[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const patrolRecords: PatrolRecord[] = [
|
||||||
|
{
|
||||||
|
id: "PR-20260623-001",
|
||||||
|
robot: "巡检机器人 R1",
|
||||||
|
startTime: "2026-06-23 08:10:22",
|
||||||
|
endTime: "2026-06-23 09:02:15",
|
||||||
|
mileage: "3.8 km",
|
||||||
|
alarms: [
|
||||||
|
{ id: "AL-001", type: "线缆破损", reportTime: "2026-06-23 08:36:12" },
|
||||||
|
{ id: "AL-002", type: "局部高温", reportTime: "2026-06-23 08:42:09" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "PR-20260623-002",
|
||||||
|
robot: "巡检机器人 Q1",
|
||||||
|
startTime: "2026-06-23 09:20:08",
|
||||||
|
endTime: "2026-06-23 10:01:46",
|
||||||
|
mileage: "2.9 km",
|
||||||
|
alarms: [
|
||||||
|
{ id: "AL-005", type: "应急门开启异常", reportTime: "2026-06-23 09:48:57" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "PR-20260623-003",
|
||||||
|
robot: "巡检机器人 N3",
|
||||||
|
startTime: "2026-06-23 10:08:34",
|
||||||
|
endTime: "2026-06-23 11:12:25",
|
||||||
|
mileage: "4.6 km",
|
||||||
|
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: "PR-20260623-004",
|
||||||
|
robot: "巡检机器人 R2",
|
||||||
|
startTime: "2026-06-23 13:15:17",
|
||||||
|
endTime: "2026-06-23 14:05:52",
|
||||||
|
mileage: "3.2 km",
|
||||||
|
alarms: [],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const dialogVisible = ref(false);
|
||||||
|
const activePatrolRecord = ref<PatrolRecord | null>(null);
|
||||||
|
|
||||||
|
function openAlarmDialog(record: PatrolRecord) {
|
||||||
|
activePatrolRecord.value = record;
|
||||||
|
dialogVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeDialog() {
|
||||||
|
dialogVisible.value = false;
|
||||||
|
activePatrolRecord.value = null;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="page-shell plan-page">
|
||||||
|
<div class="page-grid management-layout">
|
||||||
|
<section class="page-card management-table-card">
|
||||||
|
<div class="management-toolbar">
|
||||||
|
<div class="toolbar-left">
|
||||||
|
<button class="page-action-btn primary" type="button">导出记录</button>
|
||||||
|
</div>
|
||||||
|
<div class="toolbar-right">共 {{ patrolRecords.length }} 条巡检记录</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="table-shell">
|
||||||
|
<table class="device-table patrol-record-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>巡检记录号</th>
|
||||||
|
<th>巡检机器人</th>
|
||||||
|
<th>开始巡检时间</th>
|
||||||
|
<th>结束巡检时间</th>
|
||||||
|
<th>巡检里程</th>
|
||||||
|
<th>告警触发次数</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="record in patrolRecords" :key="record.id">
|
||||||
|
<td>{{ record.id }}</td>
|
||||||
|
<td>{{ record.robot }}</td>
|
||||||
|
<td>{{ record.startTime }}</td>
|
||||||
|
<td>{{ record.endTime }}</td>
|
||||||
|
<td>{{ record.mileage }}</td>
|
||||||
|
<td>
|
||||||
|
<button
|
||||||
|
class="alarm-link-btn"
|
||||||
|
type="button"
|
||||||
|
@click="openAlarmDialog(record)"
|
||||||
|
>
|
||||||
|
{{ record.alarms.length }} 次
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="dialogVisible" class="crud-dialog-mask" @click.self="closeDialog">
|
||||||
|
<div class="crud-dialog patrol-alarm-dialog">
|
||||||
|
<div class="crud-dialog-header">
|
||||||
|
<div class="crud-dialog-title">关联告警列表</div>
|
||||||
|
<button class="dialog-close" type="button" @click="closeDialog">×</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="patrol-dialog-meta">
|
||||||
|
{{ activePatrolRecord?.id }} / {{ activePatrolRecord?.robot }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="table-shell patrol-dialog-table-shell">
|
||||||
|
<table class="device-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>告警编号</th>
|
||||||
|
<th>告警类型</th>
|
||||||
|
<th>上报时间</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr
|
||||||
|
v-for="alarm in activePatrolRecord?.alarms ?? []"
|
||||||
|
:key="alarm.id"
|
||||||
|
>
|
||||||
|
<td>{{ alarm.id }}</td>
|
||||||
|
<td>{{ alarm.type }}</td>
|
||||||
|
<td>{{ alarm.reportTime }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-if="!(activePatrolRecord?.alarms.length)">
|
||||||
|
<td class="table-empty" colspan="3">本次巡检未触发告警</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
211
src/views/dashboard1/pages/VideoCenterPage.vue
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, ref } from "vue";
|
||||||
|
import monitorVideo from "../assets/循环背景动画.mp4";
|
||||||
|
|
||||||
|
type GridMode = 1 | 4 | 9;
|
||||||
|
|
||||||
|
interface CameraItem {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
status: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DeviceNode {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
cameras: CameraItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ProjectNode {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
devices: DeviceNode[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ProjectCameraItem extends CameraItem {
|
||||||
|
deviceId: string;
|
||||||
|
deviceName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const projectData: ProjectNode = {
|
||||||
|
id: "project-1",
|
||||||
|
name: "西江隧道",
|
||||||
|
devices: [
|
||||||
|
{
|
||||||
|
id: "ROBOT-001",
|
||||||
|
name: "巡检机器人 R1",
|
||||||
|
cameras: [
|
||||||
|
{ id: "CAM-001", name: "白光镜头", type: "白光", status: "在线" },
|
||||||
|
{ id: "CAM-002", name: "红外镜头", type: "红外", status: "在线" },
|
||||||
|
{ id: "CAM-003", name: "云台辅镜头", type: "辅助", status: "在线" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "ROBOT-002",
|
||||||
|
name: "巡检机器人 R2",
|
||||||
|
cameras: [
|
||||||
|
{ id: "CAM-004", name: "白光镜头", type: "白光", status: "在线" },
|
||||||
|
{ id: "CAM-005", name: "红外镜头", type: "红外", status: "在线" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const gridMode = ref<GridMode>(4);
|
||||||
|
const playbackProgress = ref(42);
|
||||||
|
const playbackDate = ref("2026-06-23");
|
||||||
|
const playbackTime = ref("14:20:00");
|
||||||
|
const activeCameraId = ref(projectData.devices[0].cameras[0].id);
|
||||||
|
|
||||||
|
const currentProject = computed(() => projectData);
|
||||||
|
|
||||||
|
const projectCameras = computed<ProjectCameraItem[]>(() =>
|
||||||
|
currentProject.value.devices.flatMap((device) =>
|
||||||
|
device.cameras.map((camera) => ({
|
||||||
|
...camera,
|
||||||
|
deviceId: device.id,
|
||||||
|
deviceName: device.name,
|
||||||
|
})),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const activeCamera = computed(
|
||||||
|
() => projectCameras.value.find((camera) => camera.id === activeCameraId.value) ?? projectCameras.value[0],
|
||||||
|
);
|
||||||
|
|
||||||
|
const orderedCameras = computed(() => {
|
||||||
|
const selected = activeCamera.value;
|
||||||
|
|
||||||
|
return projectCameras.value
|
||||||
|
.filter((camera) => camera.id !== selected.id)
|
||||||
|
.reduce<ProjectCameraItem[]>((list, camera) => {
|
||||||
|
list.push(camera);
|
||||||
|
return list;
|
||||||
|
}, [selected]);
|
||||||
|
});
|
||||||
|
|
||||||
|
const visibleCameras = computed(() => {
|
||||||
|
const countMap: Record<GridMode, number> = {
|
||||||
|
1: 1,
|
||||||
|
4: 4,
|
||||||
|
9: 9,
|
||||||
|
};
|
||||||
|
const targetCount = countMap[gridMode.value];
|
||||||
|
const cameras = orderedCameras.value;
|
||||||
|
|
||||||
|
return Array.from({ length: targetCount }, (_, index) => cameras[index] ?? null);
|
||||||
|
});
|
||||||
|
|
||||||
|
function selectCamera(cameraId: string) {
|
||||||
|
activeCameraId.value = cameraId;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setGridMode(mode: GridMode) {
|
||||||
|
gridMode.value = mode;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="page-shell video-center-page video-center-shell">
|
||||||
|
<div class="video-center-layout">
|
||||||
|
<aside class="page-card video-tree-card">
|
||||||
|
<div class="page-card-title">摄像头列表</div>
|
||||||
|
<div class="tree-panel">
|
||||||
|
<div class="tree-panel-project">
|
||||||
|
<div class="tree-panel-project-name">{{ currentProject.name }}</div>
|
||||||
|
<div class="tree-panel-project-meta">共 {{ projectCameras.length }} 路摄像头</div>
|
||||||
|
</div>
|
||||||
|
<div class="tree-device-list">
|
||||||
|
<button
|
||||||
|
v-for="camera in projectCameras"
|
||||||
|
:key="camera.id"
|
||||||
|
:class="['tree-device-btn', { active: activeCameraId === camera.id }]"
|
||||||
|
type="button"
|
||||||
|
@click="selectCamera(camera.id)"
|
||||||
|
>
|
||||||
|
<span class="tree-device-name">{{ camera.name }}</span>
|
||||||
|
<span class="tree-device-id">{{ camera.deviceName }} / {{ camera.id }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<section class="page-card video-wall-card">
|
||||||
|
<div class="video-wall-header">
|
||||||
|
<div>
|
||||||
|
<div class="page-card-title">视频播放</div>
|
||||||
|
<div class="video-wall-meta">
|
||||||
|
{{ currentProject.name }} / {{ activeCamera.name }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid-switch">
|
||||||
|
<button
|
||||||
|
v-for="mode in [1, 4, 9]"
|
||||||
|
:key="mode"
|
||||||
|
:class="['grid-switch-btn', { active: gridMode === mode }]"
|
||||||
|
type="button"
|
||||||
|
@click="setGridMode(mode as GridMode)"
|
||||||
|
>
|
||||||
|
{{ mode }} 宫格
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div :class="['video-grid-board', `grid-${gridMode}`]">
|
||||||
|
<div
|
||||||
|
v-for="(camera, index) in visibleCameras"
|
||||||
|
:key="camera?.id ?? `empty-${index}`"
|
||||||
|
class="video-grid-cell"
|
||||||
|
>
|
||||||
|
<template v-if="camera">
|
||||||
|
<video autoplay loop muted playsinline :src="monitorVideo"></video>
|
||||||
|
<div class="video-cell-top">
|
||||||
|
<span class="cell-badge">LIVE</span>
|
||||||
|
<span class="cell-type">{{ camera.type }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="video-cell-bottom">
|
||||||
|
<div class="cell-name">{{ camera.name }}</div>
|
||||||
|
<div class="cell-meta">{{ camera.deviceName }} / {{ camera.status }}</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<div class="video-empty">
|
||||||
|
<span>未分配画面</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="playback-panel">
|
||||||
|
<div class="playback-header">
|
||||||
|
<div class="playback-title">录像回放</div>
|
||||||
|
<div class="playback-filter">
|
||||||
|
<input v-model="playbackDate" type="date" />
|
||||||
|
<input v-model="playbackTime" type="time" step="1" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="playback-controls">
|
||||||
|
<button class="playback-btn" type="button">回放</button>
|
||||||
|
<button class="playback-btn" type="button">暂停</button>
|
||||||
|
<button class="playback-btn" type="button">快退</button>
|
||||||
|
<button class="playback-btn" type="button">快进</button>
|
||||||
|
<div class="playback-track">
|
||||||
|
<div class="playback-track-line">
|
||||||
|
<span :style="{ width: `${playbackProgress}%` }"></span>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
v-model="playbackProgress"
|
||||||
|
class="playback-range"
|
||||||
|
max="100"
|
||||||
|
min="0"
|
||||||
|
type="range"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="playback-time">{{ playbackDate }} {{ playbackTime }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
BIN
src/views/dashboard2/assets/card/10.png
Normal file
|
After Width: | Height: | Size: 187 KiB |
BIN
src/views/dashboard2/assets/card/10.webp
Normal file
|
After Width: | Height: | Size: 725 KiB |
BIN
src/views/dashboard2/assets/card/10_点.png
Normal file
|
After Width: | Height: | Size: 248 KiB |
BIN
src/views/dashboard2/assets/card/10_点.webp
Normal file
|
After Width: | Height: | Size: 427 KiB |
BIN
src/views/dashboard2/assets/card/标题10_静态背景.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
src/views/dashboard2/assets/center.png
Normal file
|
After Width: | Height: | Size: 398 KiB |
BIN
src/views/dashboard2/assets/右侧_动画.png
Normal file
|
After Width: | Height: | Size: 337 KiB |
BIN
src/views/dashboard2/assets/右侧_动画.webp
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
src/views/dashboard2/assets/头部动画.png
Normal file
|
After Width: | Height: | Size: 217 KiB |
BIN
src/views/dashboard2/assets/头部动画.webp
Normal file
|
After Width: | Height: | Size: 725 KiB |
BIN
src/views/dashboard2/assets/头部动画2.png
Normal file
|
After Width: | Height: | Size: 230 KiB |
BIN
src/views/dashboard2/assets/头部动画2.webp
Normal file
|
After Width: | Height: | Size: 494 KiB |
BIN
src/views/dashboard2/assets/头部动画_上升粒子-.png
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
src/views/dashboard2/assets/头部动画_上升粒子.webp
Normal file
|
After Width: | Height: | Size: 73 KiB |
BIN
src/views/dashboard2/assets/左边_动画.png
Normal file
|
After Width: | Height: | Size: 332 KiB |
BIN
src/views/dashboard2/assets/左边_动画.webp
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
src/views/dashboard2/assets/左边_闪动点.png
Normal file
|
After Width: | Height: | Size: 47 KiB |
BIN
src/views/dashboard2/assets/左边_闪动点.webp
Normal file
|
After Width: | Height: | Size: 71 KiB |
BIN
src/views/dashboard2/assets/左边_闪动点2.png
Normal file
|
After Width: | Height: | Size: 50 KiB |
BIN
src/views/dashboard2/assets/左边_闪动点2.webp
Normal file
|
After Width: | Height: | Size: 82 KiB |
BIN
src/views/dashboard2/assets/底部_动画.png
Normal file
|
After Width: | Height: | Size: 167 KiB |
BIN
src/views/dashboard2/assets/底部_动画.webp
Normal file
|
After Width: | Height: | Size: 629 KiB |
BIN
src/views/dashboard2/assets/整体边框_静态图片.png
Normal file
|
After Width: | Height: | Size: 376 KiB |
BIN
src/views/dashboard2/assets/背景_静态图片.jpg
Normal file
|
After Width: | Height: | Size: 152 KiB |
65
src/views/dashboard2/index.less
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
.dashboard {
|
||||||
|
width: 2560px;
|
||||||
|
height: 1080px;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
background-image: 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"),
|
||||||
|
url("./assets/头部动画2.png"),
|
||||||
|
url("./assets/头部动画2.webp"),
|
||||||
|
url("./assets/头部动画_上升粒子-.png"),
|
||||||
|
url("./assets/头部动画_上升粒子.webp"),
|
||||||
|
url("./assets/背景_静态图片.jpg");
|
||||||
|
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: auto;
|
||||||
|
background-position: center,
|
||||||
|
bottom, bottom, left,
|
||||||
|
left, right, right,
|
||||||
|
top, top, top,
|
||||||
|
top, top, top;
|
||||||
|
|
||||||
|
.header {
|
||||||
|
position: absolute;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
& > div {
|
||||||
|
height: 100%;
|
||||||
|
display: inline-flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > .left {
|
||||||
|
width: 20%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
& > :nth-child(1) {
|
||||||
|
flex-basis: 20%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& > .center {
|
||||||
|
width: 60%;
|
||||||
|
background-image: url("./assets/center.png");
|
||||||
|
background-size: auto;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > .right {
|
||||||
|
width: 20%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
35
src/views/dashboard2/index.vue
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import SwitchPageBtn from "@/views/dashboard1/components/btn";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="dashboard">
|
||||||
|
<div class="header">
|
||||||
|
<div class="left">
|
||||||
|
<switch-page-btn active direction="left" text="页面一" />
|
||||||
|
<switch-page-btn active direction="left" text="页面一" />
|
||||||
|
<switch-page-btn active direction="left" text="页面一" />
|
||||||
|
<switch-page-btn active direction="left" text="页面一" />
|
||||||
|
</div>
|
||||||
|
<div class="right">
|
||||||
|
<switch-page-btn active direction="left" text="页面一" />
|
||||||
|
<switch-page-btn active direction="left" text="页面一" />
|
||||||
|
<switch-page-btn active direction="left" text="页面一" />
|
||||||
|
<switch-page-btn active direction="left" text="页面一" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="main">
|
||||||
|
<div class="left">
|
||||||
|
<div class="card"></div>
|
||||||
|
<div class="card"></div>
|
||||||
|
<div class="card"></div>
|
||||||
|
</div>
|
||||||
|
<div class="center"></div>
|
||||||
|
<div class="right"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
@import "index";
|
||||||
|
</style>
|
||||||
BIN
src/views/dashboard3/assets/bg/Mask group(1).png
Normal file
|
After Width: | Height: | Size: 649 KiB |
BIN
src/views/dashboard3/assets/bg/Mask group.png
Normal file
|
After Width: | Height: | Size: 142 KiB |
BIN
src/views/dashboard3/assets/bg/Mask group@2x(1).png
Normal file
|
After Width: | Height: | Size: 3.0 MiB |
BIN
src/views/dashboard3/assets/bg/Mask group@2x.png
Normal file
|
After Width: | Height: | Size: 364 KiB |
BIN
src/views/dashboard3/assets/bg/Mask group@3x(1).png
Normal file
|
After Width: | Height: | Size: 9.7 MiB |
BIN
src/views/dashboard3/assets/bg/Mask group@3x.png
Normal file
|
After Width: | Height: | Size: 775 KiB |
BIN
src/views/dashboard3/assets/icon/Group 18.png
Normal file
|
After Width: | Height: | Size: 6.2 KiB |
BIN
src/views/dashboard3/assets/icon/Group 18@2x.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
src/views/dashboard3/assets/icon/Group 18@3x.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
src/views/dashboard3/assets/icon/Group 19.png
Normal file
|
After Width: | Height: | Size: 5.9 KiB |
BIN
src/views/dashboard3/assets/icon/Group 19@2x.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
src/views/dashboard3/assets/icon/Group 19@3x.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
src/views/dashboard3/assets/icon/Group 20.png
Normal file
|
After Width: | Height: | Size: 6.0 KiB |