first commit
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 |
BIN
src/views/dashboard3/assets/icon/Group 20@2x.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
src/views/dashboard3/assets/icon/Group 20@3x.png
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
src/views/dashboard3/assets/icon/Group 21.png
Normal file
|
After Width: | Height: | Size: 6.5 KiB |
BIN
src/views/dashboard3/assets/icon/Group 21@2x.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
src/views/dashboard3/assets/icon/Group 21@3x.png
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
src/views/dashboard3/assets/icon/Group 29.png
Normal file
|
After Width: | Height: | Size: 4.9 KiB |
BIN
src/views/dashboard3/assets/icon/Group 29@2x.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
src/views/dashboard3/assets/icon/Group 29@3x.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
src/views/dashboard3/assets/icon/Group 36.png
Normal file
|
After Width: | Height: | Size: 4.1 KiB |