init
|
|
@ -0,0 +1,2 @@
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
{
|
||||||
|
"name": "yun3v2",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@element-plus/icons-vue": "^2.3.1",
|
||||||
|
"@types/three": "^0.183.1",
|
||||||
|
"axios": "^1.6.7",
|
||||||
|
"echarts": "^6.0.0",
|
||||||
|
"element-plus": "^2.6.1",
|
||||||
|
"hls.js": "^1.6.15",
|
||||||
|
"nprogress": "^0.2.0",
|
||||||
|
"pinia": "^2.1.7",
|
||||||
|
"three": "^0.183.2",
|
||||||
|
"v3-scroll": "^1.0.4",
|
||||||
|
"vue": "^3.4.21",
|
||||||
|
"vue-echarts": "^8.0.1",
|
||||||
|
"vue-router": "^4.3.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@vitejs/plugin-vue": "^5.0.4",
|
||||||
|
"sass": "^1.71.1",
|
||||||
|
"terser": "^5.46.1",
|
||||||
|
"vite": "^5.1.6"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
# Public 目录说明
|
||||||
|
|
||||||
|
此目录下的文件在构建时会原样复制到输出目录,可通过根路径直接访问。
|
||||||
|
|
||||||
|
## 目录结构
|
||||||
|
|
||||||
|
```
|
||||||
|
public/
|
||||||
|
├── imgs/ # 静态图片资源
|
||||||
|
│ ├── logo.png
|
||||||
|
│ ├── bg.jpg
|
||||||
|
│ └── icons/
|
||||||
|
├── js/ # 第三方JS库(不通过npm管理的)
|
||||||
|
│ └── external-lib.js
|
||||||
|
├── fonts/ # 字体文件
|
||||||
|
│ └── custom-font.ttf
|
||||||
|
└── data/ # 静态数据文件
|
||||||
|
└── config.json
|
||||||
|
```
|
||||||
|
|
||||||
|
## 使用方式
|
||||||
|
|
||||||
|
在代码中直接通过根路径引用:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- HTML中 -->
|
||||||
|
<img src="/imgs/logo.png" alt="logo">
|
||||||
|
<link rel="stylesheet" href="/fonts/font.css">
|
||||||
|
|
||||||
|
<!-- Vue模板中 -->
|
||||||
|
<img :src="'/imgs/logo.png'" alt="logo">
|
||||||
|
|
||||||
|
<!-- 获取数据 -->
|
||||||
|
fetch('/data/config.json')
|
||||||
|
```
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. 此目录下的文件不会被webpack/vite处理,会直接复制
|
||||||
|
2. 适合放置不需要构建处理的静态资源
|
||||||
|
3. 大型图片、字体文件建议放这里
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
{
|
||||||
|
"appName": "上海地铁运三运营有限公司数字监控平台",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"apiBaseUrl": "/api",
|
||||||
|
"wsBaseUrl": "ws://localhost:8080/ws",
|
||||||
|
"refreshInterval": 5000,
|
||||||
|
"pageSize": 20,
|
||||||
|
"theme": {
|
||||||
|
"primaryColor": "#00d4ff",
|
||||||
|
"bgColor": "#0a1628",
|
||||||
|
"borderColor": "rgba(0, 212, 255, 0.3)"
|
||||||
|
},
|
||||||
|
"modules": {
|
||||||
|
"moduleOne": { "title": "模块一", "enabled": true },
|
||||||
|
"moduleTwo": { "title": "模块二", "enabled": true },
|
||||||
|
"moduleThree": { "title": "模块三", "enabled": true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 6.3 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 585 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 641 KiB |
|
After Width: | Height: | Size: 4.7 MiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 683 KiB |
|
After Width: | Height: | Size: 156 KiB |
|
After Width: | Height: | Size: 129 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 122 KiB |
|
After Width: | Height: | Size: 113 KiB |
|
After Width: | Height: | Size: 100 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 9.9 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 197 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 961 B |
|
After Width: | Height: | Size: 856 B |
|
After Width: | Height: | Size: 504 B |
|
|
@ -0,0 +1,9 @@
|
||||||
|
<template>
|
||||||
|
<router-view />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,52 @@
|
||||||
|
# Assets 目录说明
|
||||||
|
|
||||||
|
此目录存放需要经过构建工具处理的资源文件。
|
||||||
|
|
||||||
|
## 目录结构
|
||||||
|
|
||||||
|
```
|
||||||
|
assets/
|
||||||
|
├── imgs/ # 组件使用的图片
|
||||||
|
│ ├── avatar.png
|
||||||
|
│ ├── banner.jpg
|
||||||
|
│ └── thumbs/
|
||||||
|
├── icons/ # SVG图标
|
||||||
|
│ ├── home.svg
|
||||||
|
│ ├── user.svg
|
||||||
|
│ └── settings.svg
|
||||||
|
└── styles/ # 样式文件(已存在)
|
||||||
|
└── variables.scss
|
||||||
|
```
|
||||||
|
|
||||||
|
## 使用方式
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<template>
|
||||||
|
<!-- 使用图片 -->
|
||||||
|
<img src="@/assets/imgs/avatar.png" alt="avatar">
|
||||||
|
|
||||||
|
<!-- 使用SVG图标 -->
|
||||||
|
<svg-icon name="home" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
// 动态导入
|
||||||
|
const imgUrl = new URL('@/assets/imgs/avatar.png', import.meta.url).href
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* 在CSS中使用 */
|
||||||
|
.bg {
|
||||||
|
background: url('@/assets/imgs/banner.jpg');
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 与 public 目录的区别
|
||||||
|
|
||||||
|
| 特性 | assets | public |
|
||||||
|
|------|--------|--------|
|
||||||
|
| 构建处理 | ✅ 会被处理 | ❌ 原样复制 |
|
||||||
|
| 路径引用 | 相对路径或别名 | 根路径 |
|
||||||
|
| 适合场景 | 组件级资源 | 全局静态资源 |
|
||||||
|
| 文件大小优化 | ✅ 会被压缩 | ❌ 保持原样 |
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
// 大屏主题变量
|
||||||
|
|
||||||
|
// 主色调
|
||||||
|
$primary-color: #00d4ff;
|
||||||
|
$primary-light: #88ffff;
|
||||||
|
$primary-dark: #0088aa;
|
||||||
|
|
||||||
|
// 背景色
|
||||||
|
$bg-primary: #0a1628;
|
||||||
|
$bg-secondary: rgba(0, 50, 100, 0.2);
|
||||||
|
$bg-gradient-start: rgba(0, 100, 200, 0.3);
|
||||||
|
$bg-gradient-end: transparent;
|
||||||
|
|
||||||
|
// 边框
|
||||||
|
$border-color: rgba(0, 212, 255, 0.3);
|
||||||
|
$border-color-light: rgba(0, 212, 255, 0.2);
|
||||||
|
$border-width: 1px;
|
||||||
|
|
||||||
|
// 文字
|
||||||
|
$text-primary: #00d4ff;
|
||||||
|
$text-secondary: #7eb8d8;
|
||||||
|
$text-light: #aaddff;
|
||||||
|
|
||||||
|
// 屏幕尺寸
|
||||||
|
$screen-width: 4800px;
|
||||||
|
$screen-height: 1620px;
|
||||||
|
$header-height: 106px;
|
||||||
|
$content-height: 1514px;
|
||||||
|
|
||||||
|
// 模块标题
|
||||||
|
$module-title-height: 40px;
|
||||||
|
$module-title-bg: rgba(0, 212, 255, 0.1);
|
||||||
|
$module-title-font-size: 16px;
|
||||||
|
|
||||||
|
// 阴影/发光
|
||||||
|
$glow-primary: 0 0 20px rgba(0, 212, 255, 0.5);
|
||||||
|
$glow-secondary: 0 0 10px rgba(0, 212, 255, 0.3);
|
||||||
|
|
||||||
|
// 动画
|
||||||
|
$transition-fast: 0.2s ease;
|
||||||
|
$transition-normal: 0.3s ease;
|
||||||
|
$transition-slow: 0.5s ease;
|
||||||
|
|
@ -0,0 +1,428 @@
|
||||||
|
<template>
|
||||||
|
<div ref="canvasContainer" class="pyramid-3d-container"></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted, onUnmounted } from 'vue'
|
||||||
|
import * as THREE from 'three'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
// 是否显示光晕效果
|
||||||
|
showGlow: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
// 光晕图片路径
|
||||||
|
glowImage: {
|
||||||
|
type: String,
|
||||||
|
default: '/imgs/guangyun.png'
|
||||||
|
},
|
||||||
|
// 是否显示虚线光圈
|
||||||
|
showRing: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
// 锥体颜色
|
||||||
|
color: {
|
||||||
|
type: String,
|
||||||
|
default: '#00d4ff'
|
||||||
|
},
|
||||||
|
// 锥体大小
|
||||||
|
scale: {
|
||||||
|
type: Number,
|
||||||
|
default: 1
|
||||||
|
},
|
||||||
|
// 旋转速度
|
||||||
|
rotationSpeed: {
|
||||||
|
type: Number,
|
||||||
|
default: 0.4
|
||||||
|
},
|
||||||
|
// 是否自动旋转
|
||||||
|
autoRotate: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['loaded', 'error', 'rotate'])
|
||||||
|
|
||||||
|
const canvasContainer = ref(null)
|
||||||
|
let scene = null, camera = null, renderer = null, pyramid = null, glowRing = null, glowSprite = null
|
||||||
|
let animationId = null
|
||||||
|
let isDragging = false
|
||||||
|
let previousMousePosition = { x: 0, y: 0 }
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
initThreeJS()
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
destroy()
|
||||||
|
})
|
||||||
|
|
||||||
|
// 初始化 Three.js
|
||||||
|
const initThreeJS = () => {
|
||||||
|
const container = canvasContainer.value
|
||||||
|
if (!container) return
|
||||||
|
|
||||||
|
const width = container.clientWidth
|
||||||
|
const height = container.clientHeight
|
||||||
|
|
||||||
|
// 创建场景 - 透明背景
|
||||||
|
scene = new THREE.Scene()
|
||||||
|
scene.background = null
|
||||||
|
|
||||||
|
// 创建相机 - 调整位置以适应轻微前倾的锥体
|
||||||
|
camera = new THREE.PerspectiveCamera(45, width / height, 0.1, 1000)
|
||||||
|
camera.position.set(0, 2.0, 7)
|
||||||
|
camera.lookAt(0, 0.5, 0)
|
||||||
|
|
||||||
|
// 创建渲染器 - 透明背景
|
||||||
|
renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true })
|
||||||
|
renderer.setSize(width, height)
|
||||||
|
renderer.setPixelRatio(window.devicePixelRatio)
|
||||||
|
renderer.setClearColor(0x000000, 0)
|
||||||
|
container.appendChild(renderer.domElement)
|
||||||
|
|
||||||
|
// 创建金字塔
|
||||||
|
createPyramid()
|
||||||
|
|
||||||
|
// 创建虚线光圈
|
||||||
|
if (props.showRing) {
|
||||||
|
createDashedRing()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建光晕效果
|
||||||
|
if (props.showGlow && props.glowImage) {
|
||||||
|
createGlowEffect()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加灯光
|
||||||
|
addLights()
|
||||||
|
|
||||||
|
// 添加鼠标控制旋转
|
||||||
|
// addMouseControls()
|
||||||
|
|
||||||
|
// 开始动画
|
||||||
|
animate()
|
||||||
|
|
||||||
|
emit('loaded')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建金字塔
|
||||||
|
const createPyramid = () => {
|
||||||
|
const geometry = new THREE.ConeGeometry(1.3 * props.scale, 1.6 * props.scale, 4)
|
||||||
|
geometry.rotateY(Math.PI / 4)
|
||||||
|
// 保持尖尖在下,只做轻微前倾
|
||||||
|
geometry.rotateX(Math.PI * 0.97)
|
||||||
|
|
||||||
|
const color = new THREE.Color(props.color)
|
||||||
|
const material = new THREE.MeshPhysicalMaterial({
|
||||||
|
color: color,
|
||||||
|
emissive: color.clone().multiplyScalar(0.8),
|
||||||
|
emissiveIntensity: 0.6,
|
||||||
|
metalness: 0.9,
|
||||||
|
roughness: 0.1,
|
||||||
|
transparent: true,
|
||||||
|
opacity: 0.5,
|
||||||
|
side: THREE.DoubleSide,
|
||||||
|
transmission: 0.2,
|
||||||
|
thickness: 0.5
|
||||||
|
})
|
||||||
|
|
||||||
|
pyramid = new THREE.Mesh(geometry, material)
|
||||||
|
scene.add(pyramid)
|
||||||
|
|
||||||
|
// 内部发光核心
|
||||||
|
const coreGeometry = new THREE.ConeGeometry(0.8 * props.scale, 1.0 * props.scale, 4)
|
||||||
|
coreGeometry.rotateY(Math.PI / 4)
|
||||||
|
coreGeometry.rotateX(Math.PI * 0.9)
|
||||||
|
const coreMaterial = new THREE.MeshBasicMaterial({
|
||||||
|
color: color.clone().offsetHSL(0, 0, 0.2),
|
||||||
|
transparent: true,
|
||||||
|
opacity: 0.3
|
||||||
|
})
|
||||||
|
const core = new THREE.Mesh(coreGeometry, coreMaterial)
|
||||||
|
pyramid.add(core)
|
||||||
|
|
||||||
|
// 线框效果
|
||||||
|
const edges = new THREE.EdgesGeometry(geometry)
|
||||||
|
const lineMaterial = new THREE.LineBasicMaterial({
|
||||||
|
color: color.clone().offsetHSL(0, 0, 0.3),
|
||||||
|
transparent: true,
|
||||||
|
opacity: 0.9
|
||||||
|
})
|
||||||
|
const wireframe = new THREE.LineSegments(edges, lineMaterial)
|
||||||
|
pyramid.add(wireframe)
|
||||||
|
|
||||||
|
// 添加背景网格
|
||||||
|
// addGridHelper()
|
||||||
|
|
||||||
|
// 添加XYZ辅助线
|
||||||
|
// addAxesHelper()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建虚线光圈
|
||||||
|
const createDashedRing = () => {
|
||||||
|
const curve = new THREE.EllipseCurve(
|
||||||
|
0, 0,
|
||||||
|
1.1 * props.scale, 1.1 * props.scale,
|
||||||
|
0, 2 * Math.PI,
|
||||||
|
false,
|
||||||
|
0
|
||||||
|
)
|
||||||
|
|
||||||
|
const points = curve.getPoints(64)
|
||||||
|
const geometry = new THREE.BufferGeometry().setFromPoints(
|
||||||
|
points.map(p => new THREE.Vector3(p.x, 0, p.y))
|
||||||
|
)
|
||||||
|
|
||||||
|
const color = new THREE.Color(props.color)
|
||||||
|
const material = new THREE.LineDashedMaterial({
|
||||||
|
color: color,
|
||||||
|
dashSize: 0.12 * props.scale,
|
||||||
|
gapSize: 0.08 * props.scale,
|
||||||
|
linewidth: 2,
|
||||||
|
transparent: true,
|
||||||
|
opacity: 0.8
|
||||||
|
})
|
||||||
|
|
||||||
|
glowRing = new THREE.Line(geometry, material)
|
||||||
|
// 向上调整虚线圈位置
|
||||||
|
glowRing.position.y = 0.1 * props.scale
|
||||||
|
glowRing.computeLineDistances()
|
||||||
|
// 将虚线圈添加为三角锥体的子对象,完全同步旋转
|
||||||
|
pyramid.add(glowRing)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建光晕效果
|
||||||
|
const createGlowEffect = () => {
|
||||||
|
const textureLoader = new THREE.TextureLoader()
|
||||||
|
textureLoader.load(
|
||||||
|
props.glowImage,
|
||||||
|
(texture) => {
|
||||||
|
const color = new THREE.Color(props.color)
|
||||||
|
const spriteMaterial = new THREE.SpriteMaterial({
|
||||||
|
map: texture,
|
||||||
|
color: color,
|
||||||
|
transparent: true,
|
||||||
|
opacity: 0.6,
|
||||||
|
blending: THREE.AdditiveBlending
|
||||||
|
})
|
||||||
|
|
||||||
|
glowSprite = new THREE.Sprite(spriteMaterial)
|
||||||
|
glowSprite.scale.set(4 * props.scale, 4 * props.scale, 1)
|
||||||
|
// 调整位置以适应新的相机视角
|
||||||
|
glowSprite.position.set(0, 1.8 * props.scale, 0)
|
||||||
|
// 旋转45度,让光晕沿着正方形对角线方向(相对于三角锥体)
|
||||||
|
glowSprite.rotation.z = Math.PI / 4
|
||||||
|
// 将光晕添加为三角锥体的子对象,完全同步旋转
|
||||||
|
pyramid.add(glowSprite)
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
(error) => {
|
||||||
|
console.warn('光晕图片加载失败:', error)
|
||||||
|
emit('error', error)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加灯光
|
||||||
|
const addLights = () => {
|
||||||
|
const color = new THREE.Color(props.color)
|
||||||
|
|
||||||
|
const ambientLight = new THREE.AmbientLight(0x001133, 1.5)
|
||||||
|
scene.add(ambientLight)
|
||||||
|
|
||||||
|
const mainLight = new THREE.DirectionalLight(color, 2)
|
||||||
|
mainLight.position.set(0, 5, 3)
|
||||||
|
scene.add(mainLight)
|
||||||
|
|
||||||
|
const centerLight = new THREE.PointLight(color.clone().offsetHSL(0, 0, 0.1), 4, 8)
|
||||||
|
centerLight.position.set(0, 0.5 * props.scale, 0)
|
||||||
|
scene.add(centerLight)
|
||||||
|
|
||||||
|
const bottomLight = new THREE.PointLight(color.clone().multiplyScalar(0.5), 3, 6)
|
||||||
|
bottomLight.position.set(0, -2 * props.scale, 1)
|
||||||
|
scene.add(bottomLight)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加背景网格
|
||||||
|
const addGridHelper = () => {
|
||||||
|
const gridSize = 20
|
||||||
|
const gridDivisions = 40
|
||||||
|
const gridHelper = new THREE.GridHelper(gridSize, gridDivisions, 0x00d4ff, 0x00d4ff)
|
||||||
|
gridHelper.position.y = -2
|
||||||
|
gridHelper.material.opacity = 0.2
|
||||||
|
gridHelper.material.transparent = true
|
||||||
|
scene.add(gridHelper)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加XYZ辅助线
|
||||||
|
const addAxesHelper = () => {
|
||||||
|
const axesHelper = new THREE.AxesHelper(2)
|
||||||
|
axesHelper.position.set(0, 0, 0)
|
||||||
|
scene.add(axesHelper)
|
||||||
|
|
||||||
|
// 创建文字标签(使用CanvasTexture,不需要外部字体文件)
|
||||||
|
const createTextSprite = (message, color) => {
|
||||||
|
const canvas = document.createElement('canvas')
|
||||||
|
const ctx = canvas.getContext('2d')
|
||||||
|
ctx.font = 'Bold 60px Arial'
|
||||||
|
ctx.fillStyle = color
|
||||||
|
ctx.fillText(message, 0, 50)
|
||||||
|
|
||||||
|
const texture = new THREE.CanvasTexture(canvas)
|
||||||
|
const spriteMaterial = new THREE.SpriteMaterial({ map: texture })
|
||||||
|
const sprite = new THREE.Sprite(spriteMaterial)
|
||||||
|
sprite.scale.set(0.8, 0.8, 1.0)
|
||||||
|
|
||||||
|
return sprite
|
||||||
|
}
|
||||||
|
|
||||||
|
// X轴标签
|
||||||
|
const xText = createTextSprite('X', 0xff0000)
|
||||||
|
xText.position.set(2.2, 0, 0)
|
||||||
|
scene.add(xText)
|
||||||
|
|
||||||
|
// Y轴标签
|
||||||
|
const yText = createTextSprite('Y', 0x00ff00)
|
||||||
|
yText.position.set(0, 2.2, 0)
|
||||||
|
scene.add(yText)
|
||||||
|
|
||||||
|
// Z轴标签
|
||||||
|
const zText = createTextSprite('Z', 0x00d4ff)
|
||||||
|
zText.position.set(0, 0, 2.2)
|
||||||
|
scene.add(zText)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 动画循环
|
||||||
|
const animate = () => {
|
||||||
|
animationId = requestAnimationFrame(animate)
|
||||||
|
|
||||||
|
const time = Date.now() * 0.001
|
||||||
|
|
||||||
|
if (pyramid && props.autoRotate) {
|
||||||
|
pyramid.rotation.y = time * props.rotationSpeed
|
||||||
|
// 保持位置固定,不上下浮动
|
||||||
|
pyramid.position.y = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
renderer.render(scene, camera)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 销毁
|
||||||
|
const destroy = () => {
|
||||||
|
if (animationId) {
|
||||||
|
cancelAnimationFrame(animationId)
|
||||||
|
animationId = null
|
||||||
|
}
|
||||||
|
if (renderer) {
|
||||||
|
renderer.dispose()
|
||||||
|
renderer = null
|
||||||
|
}
|
||||||
|
scene = null
|
||||||
|
camera = null
|
||||||
|
pyramid = null
|
||||||
|
glowRing = null
|
||||||
|
glowSprite = null
|
||||||
|
}
|
||||||
|
|
||||||
|
// 暴露方法
|
||||||
|
defineExpose({
|
||||||
|
destroy
|
||||||
|
})
|
||||||
|
|
||||||
|
// 添加鼠标控制旋转
|
||||||
|
const addMouseControls = () => {
|
||||||
|
const container = canvasContainer.value
|
||||||
|
if (!container) return
|
||||||
|
|
||||||
|
// 鼠标按下
|
||||||
|
container.addEventListener('mousedown', (e) => {
|
||||||
|
isDragging = true
|
||||||
|
previousMousePosition = {
|
||||||
|
x: e.clientX,
|
||||||
|
y: e.clientY
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 鼠标移动
|
||||||
|
window.addEventListener('mousemove', (e) => {
|
||||||
|
if (!isDragging) return
|
||||||
|
|
||||||
|
const deltaX = e.clientX - previousMousePosition.x
|
||||||
|
const deltaY = e.clientY - previousMousePosition.y
|
||||||
|
|
||||||
|
previousMousePosition = {
|
||||||
|
x: e.clientX,
|
||||||
|
y: e.clientY
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pyramid) {
|
||||||
|
// 根据鼠标移动旋转金字塔
|
||||||
|
const sensitivity = 0.005
|
||||||
|
pyramid.rotation.y += deltaX * sensitivity
|
||||||
|
pyramid.rotation.x += deltaY * sensitivity
|
||||||
|
|
||||||
|
// 打印当前旋转参数
|
||||||
|
console.log('Pyramid Rotation:', {
|
||||||
|
x: pyramid.rotation.x.toFixed(4),
|
||||||
|
y: pyramid.rotation.y.toFixed(4),
|
||||||
|
z: pyramid.rotation.z.toFixed(4)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 鼠标松开
|
||||||
|
window.addEventListener('mouseup', () => {
|
||||||
|
isDragging = false
|
||||||
|
})
|
||||||
|
|
||||||
|
// 触摸控制(移动端)
|
||||||
|
container.addEventListener('touchstart', (e) => {
|
||||||
|
if (e.touches.length === 1) {
|
||||||
|
isDragging = true
|
||||||
|
previousMousePosition = {
|
||||||
|
x: e.touches[0].clientX,
|
||||||
|
y: e.touches[0].clientY
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
window.addEventListener('touchmove', (e) => {
|
||||||
|
if (!isDragging || e.touches.length !== 1) return
|
||||||
|
|
||||||
|
const deltaX = e.touches[0].clientX - previousMousePosition.x
|
||||||
|
const deltaY = e.touches[0].clientY - previousMousePosition.y
|
||||||
|
|
||||||
|
previousMousePosition = {
|
||||||
|
x: e.touches[0].clientX,
|
||||||
|
y: e.touches[0].clientY
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pyramid) {
|
||||||
|
const sensitivity = 0.005
|
||||||
|
pyramid.rotation.y += deltaX * sensitivity
|
||||||
|
pyramid.rotation.x += deltaY * sensitivity
|
||||||
|
|
||||||
|
// console.log('Pyramid Rotation:', {
|
||||||
|
// x: pyramid.rotation.x.toFixed(4),
|
||||||
|
// y: pyramid.rotation.y.toFixed(4),
|
||||||
|
// z: pyramid.rotation.z.toFixed(4)
|
||||||
|
// })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
window.addEventListener('touchend', () => {
|
||||||
|
isDragging = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.pyramid-3d-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { createApp } from 'vue'
|
||||||
|
import { createPinia } from 'pinia'
|
||||||
|
import ElementPlus from 'element-plus'
|
||||||
|
import 'element-plus/dist/index.css'
|
||||||
|
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
|
||||||
|
import App from './App.vue'
|
||||||
|
import router from './router'
|
||||||
|
import { globalScale } from './utils/scale'
|
||||||
|
import './styles/global.scss'
|
||||||
|
|
||||||
|
const app = createApp(App)
|
||||||
|
|
||||||
|
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
||||||
|
app.component(key, component)
|
||||||
|
}
|
||||||
|
|
||||||
|
app.use(createPinia())
|
||||||
|
app.use(router)
|
||||||
|
app.use(ElementPlus)
|
||||||
|
|
||||||
|
app.mount('#app')
|
||||||
|
|
||||||
|
globalScale()
|
||||||
|
|
@ -0,0 +1,74 @@
|
||||||
|
import { createRouter, createWebHashHistory } from 'vue-router'
|
||||||
|
import NProgress from 'nprogress'
|
||||||
|
|
||||||
|
NProgress.configure({ showSpinner: false })
|
||||||
|
|
||||||
|
const routes = [
|
||||||
|
{
|
||||||
|
path: '/login',
|
||||||
|
name: 'Login',
|
||||||
|
component: () => import('@/views/login/index.vue'),
|
||||||
|
meta: { title: '登录', requiresAuth: false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
name: 'Layout',
|
||||||
|
component: () => import('@/views/layout/index.vue'),
|
||||||
|
redirect: '/home',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: 'home',
|
||||||
|
name: 'Home',
|
||||||
|
component: () => import('@/views/home/index.vue'),
|
||||||
|
meta: { title: '首页', requiresAuth: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'dashboard',
|
||||||
|
name: 'Dashboard',
|
||||||
|
component: () => import('@/views/dashboard/index.vue'),
|
||||||
|
meta: { title: '数据大屏', requiresAuth: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'traffic',
|
||||||
|
name: 'Traffic',
|
||||||
|
component: () => import('@/views/traffic/index.vue'),
|
||||||
|
meta: { title: '客运', requiresAuth: true }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
meta: { requiresAuth: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/404',
|
||||||
|
name: '404',
|
||||||
|
component: () => import('@/views/error/404.vue'),
|
||||||
|
meta: { title: '404', requiresAuth: false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/500',
|
||||||
|
name: '500',
|
||||||
|
component: () => import('@/views/error/500.vue'),
|
||||||
|
meta: { title: '500', requiresAuth: false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/:pathMatch(.*)*',
|
||||||
|
redirect: '/404'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const router = createRouter({
|
||||||
|
history: createWebHashHistory(),
|
||||||
|
routes
|
||||||
|
})
|
||||||
|
|
||||||
|
router.beforeEach((to, from, next) => {
|
||||||
|
NProgress.start()
|
||||||
|
document.title = to.meta.title ? `${to.meta.title} - 云智控` : '云智控大屏系统'
|
||||||
|
|
||||||
|
next()
|
||||||
|
})
|
||||||
|
|
||||||
|
router.afterEach(() => {
|
||||||
|
NProgress.done()
|
||||||
|
})
|
||||||
|
|
||||||
|
export default router
|
||||||
|
|
@ -0,0 +1,103 @@
|
||||||
|
:root {
|
||||||
|
--primary-color: #00d4ff;
|
||||||
|
--bg-dark: #0a1628;
|
||||||
|
--bg-card: rgba(0, 212, 255, 0.05);
|
||||||
|
--border-color: rgba(0, 212, 255, 0.3);
|
||||||
|
--text-primary: #e8f4ff;
|
||||||
|
--text-secondary: #7eb8d8;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Alibaba PuHuiTi 3.0';
|
||||||
|
src: url('/fonts/AlibabaPuHuiTi-3-65-Medium.woff2') format('woff2');
|
||||||
|
font-weight: 500;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html, body, #app {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
font-family: 'Alibaba PuHuiTi 3.0', 'Microsoft YaHei', 'PingFang SC', sans-serif;
|
||||||
|
background: var(--bg-dark);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
button,
|
||||||
|
input,
|
||||||
|
textarea,
|
||||||
|
select {
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-container {
|
||||||
|
width: 4800px;
|
||||||
|
height: 1600px;
|
||||||
|
transform-origin: top left;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex-center {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex-between {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass-card {
|
||||||
|
background: var(--bg-card);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--primary-color);
|
||||||
|
padding-bottom: 12px;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-message-box {
|
||||||
|
background: var(--bg-dark) !important;
|
||||||
|
border-color: var(--border-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-input__wrapper {
|
||||||
|
background: rgba(0, 212, 255, 0.1) !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
border: 1px solid var(--border-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-input__inner {
|
||||||
|
color: var(--text-primary) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--primary-color);
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
/**
|
||||||
|
* 静态资源加载工具
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取public目录下的图片URL
|
||||||
|
* @param {string} path - 图片路径,如 'imgs/logo.png'
|
||||||
|
* @returns {string} 完整URL
|
||||||
|
*/
|
||||||
|
export const getPublicImageUrl = (path) => {
|
||||||
|
return `/${path}`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取assets目录下的图片URL(用于动态导入)
|
||||||
|
* @param {string} path - 图片路径,如 'imgs/avatar.png'
|
||||||
|
* @returns {string} 完整URL
|
||||||
|
*/
|
||||||
|
export const getAssetImageUrl = (path) => {
|
||||||
|
return new URL(`@/assets/${path}`, import.meta.url).href
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 预加载图片
|
||||||
|
* @param {string[]} urls - 图片URL数组
|
||||||
|
* @returns {Promise} 加载完成Promise
|
||||||
|
*/
|
||||||
|
export const preloadImages = (urls) => {
|
||||||
|
const promises = urls.map(url => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const img = new Image()
|
||||||
|
img.onload = () => resolve(url)
|
||||||
|
img.onerror = () => reject(url)
|
||||||
|
img.src = url
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return Promise.all(promises)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载public/data下的JSON数据
|
||||||
|
* @param {string} filename - 文件名,如 'config.json'
|
||||||
|
* @returns {Promise} JSON数据
|
||||||
|
*/
|
||||||
|
export const loadJsonData = async (filename) => {
|
||||||
|
const response = await fetch(`/data/${filename}`)
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to load ${filename}`)
|
||||||
|
}
|
||||||
|
return response.json()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 常用图片路径常量
|
||||||
|
*/
|
||||||
|
export const IMAGES = {
|
||||||
|
// public/imgs 目录下的图片
|
||||||
|
LOGO: '/imgs/logo.png',
|
||||||
|
BG_MAIN: '/imgs/bg.jpg',
|
||||||
|
BG_HEADER: '/imgs/header-bg.png',
|
||||||
|
|
||||||
|
// assets/imgs 目录下的图片
|
||||||
|
AVATAR_DEFAULT: '@/assets/imgs/avatar.png',
|
||||||
|
ICON_HOME: '@/assets/icons/home.svg'
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
import { encrypt, decrypt } from './sm4'
|
||||||
|
|
||||||
|
export { encrypt, decrypt }
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
export function formatNumber(num, precision = 2) {
|
||||||
|
if (!num && num !== 0) return '--'
|
||||||
|
return Number(num).toFixed(precision)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatDate(date, format = 'YYYY-MM-DD HH:mm:ss') {
|
||||||
|
if (!date) return '--'
|
||||||
|
const d = new Date(date)
|
||||||
|
const year = d.getFullYear()
|
||||||
|
const month = String(d.getMonth() + 1).padStart(2, '0')
|
||||||
|
const day = String(d.getDate()).padStart(2, '0')
|
||||||
|
const hours = String(d.getHours()).padStart(2, '0')
|
||||||
|
const minutes = String(d.getMinutes()).padStart(2, '0')
|
||||||
|
const seconds = String(d.getSeconds()).padStart(2, '0')
|
||||||
|
|
||||||
|
return format
|
||||||
|
.replace('YYYY', year)
|
||||||
|
.replace('MM', month)
|
||||||
|
.replace('DD', day)
|
||||||
|
.replace('HH', hours)
|
||||||
|
.replace('mm', minutes)
|
||||||
|
.replace('ss', seconds)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatBytes(bytes, precision = 2) {
|
||||||
|
if (!bytes || bytes === 0) return '0 B'
|
||||||
|
const k = 1024
|
||||||
|
const sizes = ['B', 'KB', 'MB', 'GB', 'TB']
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||||
|
return `${(bytes / Math.pow(k, i)).toFixed(precision)} ${sizes[i]}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function debounce(fn, delay = 300) {
|
||||||
|
let timer = null
|
||||||
|
return function (...args) {
|
||||||
|
if (timer) clearTimeout(timer)
|
||||||
|
timer = setTimeout(() => fn.apply(this, args), delay)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function throttle(fn, delay = 300) {
|
||||||
|
let last = 0
|
||||||
|
return function (...args) {
|
||||||
|
const now = Date.now()
|
||||||
|
if (now - last > delay) {
|
||||||
|
last = now
|
||||||
|
fn.apply(this, args)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,86 @@
|
||||||
|
import axios from 'axios'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import router from '@/router'
|
||||||
|
import { encrypt, decrypt } from './sm4'
|
||||||
|
|
||||||
|
const service = axios.create({
|
||||||
|
baseURL: import.meta.env.VITE_API_BASE_URL || '/api',
|
||||||
|
timeout: 30000
|
||||||
|
})
|
||||||
|
|
||||||
|
service.interceptors.request.use(
|
||||||
|
config => {
|
||||||
|
const token = localStorage.getItem('token')
|
||||||
|
if (token) {
|
||||||
|
config.headers.Authorization = `Bearer ${token}`
|
||||||
|
}
|
||||||
|
if (config.method === 'post' || config.method === 'put') {
|
||||||
|
const data = typeof config.data === 'string' ? config.data : JSON.stringify(config.data)
|
||||||
|
config.data = { data: encrypt(data) }
|
||||||
|
}
|
||||||
|
return config
|
||||||
|
},
|
||||||
|
error => {
|
||||||
|
return Promise.reject(error)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
service.interceptors.response.use(
|
||||||
|
response => {
|
||||||
|
const res = response.data
|
||||||
|
if (response.config.responseType === 'blob') {
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
if (res.code !== 200 && res.code !== 0) {
|
||||||
|
ElMessage.error(res.message || '请求失败')
|
||||||
|
return Promise.reject(new Error(res.message || '请求失败'))
|
||||||
|
}
|
||||||
|
if (res.data && typeof res.data === 'string') {
|
||||||
|
try {
|
||||||
|
res.data = JSON.parse(decrypt(res.data))
|
||||||
|
} catch (e) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
},
|
||||||
|
error => {
|
||||||
|
if (error.response) {
|
||||||
|
switch (error.response.status) {
|
||||||
|
case 401:
|
||||||
|
ElMessage.error('登录已过期,请重新登录')
|
||||||
|
localStorage.removeItem('token')
|
||||||
|
router.push('/login')
|
||||||
|
break
|
||||||
|
case 403:
|
||||||
|
ElMessage.error('没有权限访问')
|
||||||
|
break
|
||||||
|
case 404:
|
||||||
|
ElMessage.error('请求资源不存在')
|
||||||
|
break
|
||||||
|
case 500:
|
||||||
|
router.push('/500')
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
ElMessage.error(error.message || '网络错误')
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ElMessage.error('网络连接失败')
|
||||||
|
}
|
||||||
|
return Promise.reject(error)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export default service
|
||||||
|
|
||||||
|
export const request = {
|
||||||
|
get: (url, params, config = {}) => service.get(url, { params, ...config }),
|
||||||
|
post: (url, data, config = {}) => service.post(url, data, config),
|
||||||
|
put: (url, data, config = {}) => service.put(url, data, config),
|
||||||
|
delete: (url, params, config = {}) => service.delete(url, { params, ...config }),
|
||||||
|
download: (url, data, config = {}) => service.post(url, data, { responseType: 'blob', ...config }),
|
||||||
|
upload: (url, data, config = {}) => {
|
||||||
|
const formData = new FormData()
|
||||||
|
Object.keys(data).forEach(key => formData.append(key, data[key]))
|
||||||
|
return service.post(url, formData, { headers: { 'Content-Type': 'multipart/form-data' }, ...config })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
export function globalScale() {
|
||||||
|
const baseWidth = 4800
|
||||||
|
const baseHeight = 1620
|
||||||
|
|
||||||
|
function setScale() {
|
||||||
|
const width = window.innerWidth
|
||||||
|
const height = window.innerHeight
|
||||||
|
const scaleX = width / baseWidth
|
||||||
|
const scaleY = height / baseHeight
|
||||||
|
const scale = Math.min(scaleX, scaleY, 1)
|
||||||
|
|
||||||
|
const app = document.getElementById('app')
|
||||||
|
if (app) {
|
||||||
|
app.style.transform = `scale(${scale})`
|
||||||
|
app.style.transformOrigin = 'left top'
|
||||||
|
app.style.width = `${baseWidth}px`
|
||||||
|
app.style.height = `${baseHeight}px`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setScale()
|
||||||
|
window.addEventListener('resize', setScale)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getScale() {
|
||||||
|
const width = window.innerWidth
|
||||||
|
const height = window.innerHeight
|
||||||
|
return {
|
||||||
|
scaleX: width / 4800,
|
||||||
|
scaleY: height / 1620,
|
||||||
|
scale: Math.min(width / 4800, height / 1620, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,177 @@
|
||||||
|
import { request } from './request'
|
||||||
|
import { encrypt } from './decrypt'
|
||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
var ipRoot = "http://140.206.138.190:8900"
|
||||||
|
var ip = "http://140.206.138.190:8905"
|
||||||
|
var ysdpmIp = import.meta.env.VITE_API_BASE_URL || "http://192.168.16.193:8888"
|
||||||
|
|
||||||
|
var getToken = "/app/getToken"
|
||||||
|
var getTblMetroInout = ipRoot + '/api/xckf/getTblMetroInout/v1'
|
||||||
|
var getDsjzxQxjGdybHour = ipRoot + '/api/xckf/getDsjzxQxjGdybHour/v1'
|
||||||
|
var getOperationInfo = ipRoot + '/api/xckf/getOperationInfo/v1'
|
||||||
|
var getWeiboCards = ipRoot + '/api/xckf/getWeiboCards/v1'
|
||||||
|
var getWeiboComments = ipRoot + '/api/xckf/getWeiboComments/v1'
|
||||||
|
var getWeiboCardsComment = ipRoot + '/api/xckf/getWeiboCardsComment/v1'
|
||||||
|
var getMetroSsxEmergency = ipRoot + '/api/xckf/getMetroSsxEmergency/v1'
|
||||||
|
var getYjEntrance = ipRoot + '/api/xckf/getYjEntrance/v1'
|
||||||
|
var updayeYjEntrance = ipRoot + '/api/xckf/updayeYjEntrance/v1'
|
||||||
|
var getGradeSubwayTarget = ipRoot + '/api/xckf/getGradeSubwayTarget/v1'
|
||||||
|
var getTos = ipRoot + '/api/xckf/getTos/v1'
|
||||||
|
var getBusInfo = ipRoot + '/api/xckf/getBusInfo/v1'
|
||||||
|
var updayeBusEntrance = ipRoot + '/api/xckf/updayeBusEntrance/v1'
|
||||||
|
var getHotEventList = ipRoot + '/api/xckf/getHotEventList/v1'
|
||||||
|
var updateHotEvent = ipRoot + '/api/xckf/updateHotEvent/v1'
|
||||||
|
var getOperateInfo = (import.meta.env.DEV ? '' : import.meta.env.VITE_API_BASE_URL) + '/juntech-ysdp/api/getOperateInfo/v1'
|
||||||
|
var getCommonMonitor = (import.meta.env.DEV ? '' : import.meta.env.VITE_API_BASE_URL) + '/juntech-ysdp/api/getCommonMonitor/v1'
|
||||||
|
var getDutyList = (import.meta.env.DEV ? '' : import.meta.env.VITE_API_BASE_URL) + '/juntech-ysdp/api/getDutyList/v1'
|
||||||
|
var getStationMaxList = (import.meta.env.DEV ? '' : import.meta.env.VITE_API_BASE_URL) + '/juntech-ysdp/api/getStationMaxList/v1'
|
||||||
|
var getIndicator = (import.meta.env.DEV ? '' : import.meta.env.VITE_API_BASE_URL) + '/juntech-ysdp/api/getIndicator/v1'
|
||||||
|
var getYunyingInfo = (import.meta.env.DEV ? '' : import.meta.env.VITE_API_BASE_URL) + '/juntech-ysdp/api/getYunyingInfo/v1'
|
||||||
|
var getQxjGdybHour = (import.meta.env.DEV ? '' : import.meta.env.VITE_API_BASE_URL) + '/juntech-ysdp/api/getQxjGdybHour/v1'
|
||||||
|
var getTaskNotice = (import.meta.env.DEV ? '' : import.meta.env.VITE_API_BASE_URL) + '/juntech-ysdp/api/getTaskNotice/v1'
|
||||||
|
var xcImg = ip + '/1734.jpg'
|
||||||
|
var xcImgGd = ip + '/lineCrossImg/1734.jpg'
|
||||||
|
|
||||||
|
var server = {
|
||||||
|
getMetroUrl() {
|
||||||
|
return "http://140.206.138.190:8899/subway/#/metro"
|
||||||
|
},
|
||||||
|
|
||||||
|
getIp() {
|
||||||
|
return ipRoot
|
||||||
|
},
|
||||||
|
|
||||||
|
getXcImg() {
|
||||||
|
return xcImg
|
||||||
|
},
|
||||||
|
|
||||||
|
getXcImgGd() {
|
||||||
|
return xcImgGd
|
||||||
|
},
|
||||||
|
|
||||||
|
getToken() {
|
||||||
|
return request.get(ipRoot + getToken, {})
|
||||||
|
},
|
||||||
|
|
||||||
|
getTblMetroInout() {
|
||||||
|
return request.post(getTblMetroInout, {})
|
||||||
|
},
|
||||||
|
|
||||||
|
getDsjzxQxjGdybHour() {
|
||||||
|
return request.post(getDsjzxQxjGdybHour, {})
|
||||||
|
},
|
||||||
|
|
||||||
|
getOperationInfo() {
|
||||||
|
return request.post(getOperationInfo, {
|
||||||
|
statId: ""
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
getWeiboCards(keyword, pageNum, pageSize) {
|
||||||
|
return request.post(getWeiboCards, {
|
||||||
|
keyword: encrypt(keyword),
|
||||||
|
pageNum: encrypt(pageNum),
|
||||||
|
pageSize: encrypt(pageSize)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
getWeiboComments(keyword, pageNum, pageSize) {
|
||||||
|
return request.post(getWeiboComments, {
|
||||||
|
keyword: encrypt(keyword),
|
||||||
|
pageNum: encrypt(pageNum),
|
||||||
|
pageSize: encrypt(pageSize)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
getWeiboCardsComment() {
|
||||||
|
return request.post(getWeiboCardsComment, {})
|
||||||
|
},
|
||||||
|
|
||||||
|
getMetroSsxEmergency() {
|
||||||
|
return request.post(getMetroSsxEmergency, {})
|
||||||
|
},
|
||||||
|
|
||||||
|
getYjEntrance(statId) {
|
||||||
|
return request.post(getYjEntrance, {
|
||||||
|
statId: encrypt(statId)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
updayeYjEntrance(statId, entrance) {
|
||||||
|
return request.post(updayeYjEntrance, {
|
||||||
|
statId: encrypt(statId),
|
||||||
|
entrance: encrypt(entrance)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
getGradeSubwayTarget(statId, nf, jd) {
|
||||||
|
return request.post(getGradeSubwayTarget, {
|
||||||
|
statId: encrypt(statId),
|
||||||
|
nf: encrypt(nf),
|
||||||
|
jd: encrypt(jd)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
getTos() {
|
||||||
|
return request.post(getTos, {})
|
||||||
|
},
|
||||||
|
|
||||||
|
getBusInfo(statId) {
|
||||||
|
return request.post(getBusInfo, {
|
||||||
|
statId: encrypt(statId)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
updayeBusEntrance(statId, entrance) {
|
||||||
|
return request.post(updayeBusEntrance, {
|
||||||
|
statId: encrypt(statId),
|
||||||
|
entrance: encrypt(entrance)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
getHotEventList() {
|
||||||
|
return request.post(getHotEventList, {})
|
||||||
|
},
|
||||||
|
|
||||||
|
updateHotEvent(id, status) {
|
||||||
|
return request.post(updateHotEvent, {
|
||||||
|
id: encrypt(id),
|
||||||
|
status: encrypt(status)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
getOperateInfo() {
|
||||||
|
return axios.post(getOperateInfo, {})
|
||||||
|
},
|
||||||
|
|
||||||
|
getCommonMonitor() {
|
||||||
|
return axios.post(getCommonMonitor, {})
|
||||||
|
},
|
||||||
|
|
||||||
|
getDutyList() {
|
||||||
|
return axios.post(getDutyList, {})
|
||||||
|
},
|
||||||
|
|
||||||
|
getStationMaxList() {
|
||||||
|
return axios.post(getStationMaxList, {})
|
||||||
|
},
|
||||||
|
|
||||||
|
getIndicator() {
|
||||||
|
return axios.post(getIndicator, {})
|
||||||
|
},
|
||||||
|
|
||||||
|
getQxjGdybHour() {
|
||||||
|
return axios.post(getQxjGdybHour, {})
|
||||||
|
},
|
||||||
|
|
||||||
|
getYunyingInfo() {
|
||||||
|
return axios.post(getYunyingInfo, {})
|
||||||
|
},
|
||||||
|
|
||||||
|
getTaskNotice() {
|
||||||
|
return axios.post(getTaskNotice, {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default server
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
import crypto from 'crypto'
|
||||||
|
|
||||||
|
const defaultKey = 'ThksAZfIBu4GCPd6'
|
||||||
|
const defaultIv = 'ThksAZfIBu4GCPd6'
|
||||||
|
|
||||||
|
function encrypt(text, key = defaultKey, iv = defaultIv) {
|
||||||
|
if (!text) return ''
|
||||||
|
try {
|
||||||
|
const cipher = crypto.createCipheriv('sm4-cbc', Buffer.from(key), Buffer.from(iv))
|
||||||
|
let encrypted = cipher.update(text, 'utf8', 'base64')
|
||||||
|
encrypted += cipher.final('base64')
|
||||||
|
return encrypted
|
||||||
|
} catch (e) {
|
||||||
|
console.error('SM4 encrypt error:', e)
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function decrypt(encrypted, key = defaultKey, iv = defaultIv) {
|
||||||
|
if (!encrypted) return ''
|
||||||
|
try {
|
||||||
|
const decipher = crypto.createDecipheriv('sm4-cbc', Buffer.from(key), Buffer.from(iv))
|
||||||
|
let decrypted = decipher.update(encrypted, 'base64', 'utf8')
|
||||||
|
decrypted += decipher.final('utf8')
|
||||||
|
return decrypted
|
||||||
|
} catch (e) {
|
||||||
|
console.error('SM4 decrypt error:', e)
|
||||||
|
return encrypted
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function encryptData(data, key = defaultKey, iv = defaultIv) {
|
||||||
|
if (!data) return ''
|
||||||
|
const text = typeof data === 'string' ? data : JSON.stringify(data)
|
||||||
|
return encrypt(text, key, iv)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function decryptData(encrypted, key = defaultKey, iv = defaultIv) {
|
||||||
|
if (!encrypted) return null
|
||||||
|
try {
|
||||||
|
const decrypted = decrypt(encrypted, key, iv)
|
||||||
|
return JSON.parse(decrypted)
|
||||||
|
} catch (e) {
|
||||||
|
return decrypted
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { encrypt, decrypt }
|
||||||
|
|
@ -0,0 +1,382 @@
|
||||||
|
<template>
|
||||||
|
<div class="dashboard-container">
|
||||||
|
<div class="dashboard">
|
||||||
|
<div class="dashboard-header">
|
||||||
|
<h2>数据监控大屏</h2>
|
||||||
|
<div class="header-time">{{ currentTime }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dashboard-content">
|
||||||
|
<div class="content-left">
|
||||||
|
<div class="panel">
|
||||||
|
<div class="panel-header">
|
||||||
|
<span class="panel-title">实时数据监控</span>
|
||||||
|
</div>
|
||||||
|
<div class="panel-content">
|
||||||
|
<div class="data-list">
|
||||||
|
<div class="data-item" v-for="item in monitorData" :key="item.label">
|
||||||
|
<span class="data-label">{{ item.label }}</span>
|
||||||
|
<span class="data-value" :style="{ color: item.color }">{{ item.value }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel">
|
||||||
|
<div class="panel-header">
|
||||||
|
<span class="panel-title">数据趋势</span>
|
||||||
|
</div>
|
||||||
|
<div class="panel-content chart-area">
|
||||||
|
<div class="chart-placeholder">
|
||||||
|
<el-icon :size="48">
|
||||||
|
<TrendCharts />
|
||||||
|
</el-icon>
|
||||||
|
<p>趋势图表区域</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content-center">
|
||||||
|
<div class="center-panel">
|
||||||
|
<div class="center-number">
|
||||||
|
<span class="number-value">{{ mainData.count }}</span>
|
||||||
|
<span class="number-unit">{{ mainData.unit }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="center-label">{{ mainData.label }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="center-grid">
|
||||||
|
<div class="grid-item" v-for="item in gridData" :key="item.label">
|
||||||
|
<span class="grid-value">{{ item.value }}</span>
|
||||||
|
<span class="grid-label">{{ item.label }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content-right">
|
||||||
|
<div class="panel">
|
||||||
|
<div class="panel-header">
|
||||||
|
<span class="panel-title">数据排名</span>
|
||||||
|
</div>
|
||||||
|
<div class="panel-content">
|
||||||
|
<div class="rank-list">
|
||||||
|
<div class="rank-item" v-for="(item, index) in rankData" :key="index">
|
||||||
|
<span class="rank-index" :class="{ top: index < 3 }">{{ index + 1 }}</span>
|
||||||
|
<span class="rank-name">{{ item.name }}</span>
|
||||||
|
<span class="rank-value">{{ item.value }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel">
|
||||||
|
<div class="panel-header">
|
||||||
|
<span class="panel-title">占比分析</span>
|
||||||
|
</div>
|
||||||
|
<div class="panel-content chart-area">
|
||||||
|
<div class="chart-placeholder">
|
||||||
|
<el-icon :size="48">
|
||||||
|
<PieChart />
|
||||||
|
</el-icon>
|
||||||
|
<p>饼图区域</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted, onUnmounted } from 'vue'
|
||||||
|
import { TrendCharts, PieChart } from '@element-plus/icons-vue'
|
||||||
|
|
||||||
|
const currentTime = ref('')
|
||||||
|
let timer = null
|
||||||
|
|
||||||
|
const updateTime = () => {
|
||||||
|
const now = new Date()
|
||||||
|
currentTime.value = now.toLocaleString('zh-CN', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
second: '2-digit'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const monitorData = ref([
|
||||||
|
{ label: 'CPU 使用率', value: '67.8%', color: '#00d4ff' },
|
||||||
|
{ label: '内存使用率', value: '82.3%', color: '#ff9f43' },
|
||||||
|
{ label: '磁盘使用率', value: '45.6%', color: '#00d4ff' },
|
||||||
|
{ label: '网络带宽', value: '1.2 Gbps', color: '#00d4ff' },
|
||||||
|
{ label: '在线设备', value: '1 台', color: '#00d4ff' },
|
||||||
|
{ label: '今日访问', value: '0 次', color: '#00d4ff' }
|
||||||
|
])
|
||||||
|
|
||||||
|
const mainData = ref({
|
||||||
|
count: 69.8,
|
||||||
|
unit: '%',
|
||||||
|
label: '系统运行率'
|
||||||
|
})
|
||||||
|
|
||||||
|
const gridData = ref([
|
||||||
|
{ label: '总数据量', value: '12.8 PB' },
|
||||||
|
{ label: '日处理量', value: '128 万' },
|
||||||
|
{ label: '平均响应', value: '23 ms' },
|
||||||
|
{ label: '可用率', value: '99.99%' }
|
||||||
|
])
|
||||||
|
|
||||||
|
const rankData = ref([
|
||||||
|
{ name: '华东地区', value: '23,456' },
|
||||||
|
{ name: '华南地区', value: '21,234' },
|
||||||
|
{ name: '华北地区', value: '19,876' },
|
||||||
|
{ name: '西南地区', value: '15,432' },
|
||||||
|
{ name: '西北地区', value: '12,345' }
|
||||||
|
])
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
updateTime()
|
||||||
|
timer = setInterval(updateTime, 1000)
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (timer) clearInterval(timer)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.dashboard-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
padding: 24px;
|
||||||
|
background: var(--bg-dark);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 16px 32px;
|
||||||
|
background: linear-gradient(90deg, rgba(0, 212, 255, 0.2), transparent);
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-time {
|
||||||
|
font-size: 20px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-family: 'DIN Alternate', sans-serif;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-content {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
gap: 24px;
|
||||||
|
margin-top: 24px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-left,
|
||||||
|
.content-right {
|
||||||
|
width: 400px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-center {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel {
|
||||||
|
background: var(--bg-card);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.panel-header {
|
||||||
|
padding: 16px 20px;
|
||||||
|
background: rgba(0, 212, 255, 0.1);
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
|
||||||
|
.panel-title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-content {
|
||||||
|
padding: 20px;
|
||||||
|
|
||||||
|
&.chart-area {
|
||||||
|
height: 200px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-placeholder {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin-top: 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-list {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 16px;
|
||||||
|
|
||||||
|
.data-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: rgba(0, 0, 0, 0.2);
|
||||||
|
border-radius: 4px;
|
||||||
|
|
||||||
|
.data-label {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-value {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.center-panel {
|
||||||
|
text-align: center;
|
||||||
|
padding: 48px 0;
|
||||||
|
|
||||||
|
.center-number {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
.number-value {
|
||||||
|
font-size: 120px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--primary-color);
|
||||||
|
line-height: 1;
|
||||||
|
text-shadow: 0 0 40px rgba(0, 212, 255, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.number-unit {
|
||||||
|
font-size: 48px;
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.center-label {
|
||||||
|
font-size: 24px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.center-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 24px;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 600px;
|
||||||
|
|
||||||
|
.grid-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding: 24px;
|
||||||
|
background: var(--bg-card);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
|
.grid-value {
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-label {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.rank-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
|
.rank-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: rgba(0, 0, 0, 0.2);
|
||||||
|
border-radius: 4px;
|
||||||
|
|
||||||
|
.rank-index {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: rgba(0, 212, 255, 0.2);
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
|
||||||
|
&.top {
|
||||||
|
background: var(--primary-color);
|
||||||
|
color: var(--bg-dark);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.rank-name {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rank-value {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,73 @@
|
||||||
|
<template>
|
||||||
|
<div class="error-container">
|
||||||
|
<div class="error-content">
|
||||||
|
<div class="error-code">404</div>
|
||||||
|
<div class="error-message">抱歉,您访问的页面不存在</div>
|
||||||
|
<div class="error-description">
|
||||||
|
可能是因为链接错误、页面已删除或正在维护中
|
||||||
|
</div>
|
||||||
|
<el-button type="primary" size="large" @click="goHome">
|
||||||
|
返回首页
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const goHome = () => {
|
||||||
|
router.push('/home')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.error-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: var(--bg-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-content {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-code {
|
||||||
|
font-size: 180px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--primary-color);
|
||||||
|
line-height: 1;
|
||||||
|
text-shadow: 0 4px 20px rgba(0, 212, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-message {
|
||||||
|
font-size: 32px;
|
||||||
|
color: var(--text-primary);
|
||||||
|
margin-top: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-description {
|
||||||
|
font-size: 16px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin-top: 16px;
|
||||||
|
margin-bottom: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-button {
|
||||||
|
padding: 16px 48px;
|
||||||
|
font-size: 16px;
|
||||||
|
background: var(--primary-color);
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
color: var(--bg-dark);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #33ddff;
|
||||||
|
border-color: #33ddff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,73 @@
|
||||||
|
<template>
|
||||||
|
<div class="error-container">
|
||||||
|
<div class="error-content">
|
||||||
|
<div class="error-code">500</div>
|
||||||
|
<div class="error-message">抱歉,服务器发生了错误</div>
|
||||||
|
<div class="error-description">
|
||||||
|
请稍后重试或联系管理员
|
||||||
|
</div>
|
||||||
|
<el-button type="primary" size="large" @click="goHome">
|
||||||
|
返回首页
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const goHome = () => {
|
||||||
|
router.push('/home')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.error-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: var(--bg-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-content {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-code {
|
||||||
|
font-size: 180px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #ff4d4f;
|
||||||
|
line-height: 1;
|
||||||
|
text-shadow: 0 4px 20px rgba(255, 77, 79, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-message {
|
||||||
|
font-size: 32px;
|
||||||
|
color: var(--text-primary);
|
||||||
|
margin-top: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-description {
|
||||||
|
font-size: 16px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin-top: 16px;
|
||||||
|
margin-bottom: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-button {
|
||||||
|
padding: 16px 48px;
|
||||||
|
font-size: 16px;
|
||||||
|
background: var(--primary-color);
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
color: var(--bg-dark);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #33ddff;
|
||||||
|
border-color: #33ddff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,211 @@
|
||||||
|
<template>
|
||||||
|
<div class="dashboard-header">
|
||||||
|
<div class="header-title">
|
||||||
|
<img src="/imgs/title.png" alt="title" class="title-img" />
|
||||||
|
</div>
|
||||||
|
<div class="weather-info">
|
||||||
|
<div class="weather-item">
|
||||||
|
<span class="label">天气:</span>
|
||||||
|
<span class="value">{{ weatherInfo.weather || '--' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="weather-item">
|
||||||
|
<span class="label">温度:</span>
|
||||||
|
<span class="value">{{ weatherInfo.tem || '--' }}℃</span>
|
||||||
|
</div>
|
||||||
|
<div class="weather-item">
|
||||||
|
<span class="label">风速:</span>
|
||||||
|
<span class="value">{{ weatherInfo.wind_speed || '--' }}m/s</span>
|
||||||
|
</div>
|
||||||
|
<div class="weather-item">
|
||||||
|
<span class="label">湿度:</span>
|
||||||
|
<span class="value">{{ weatherInfo.rh || '--' }}%</span>
|
||||||
|
</div>
|
||||||
|
<div class="weather-item">
|
||||||
|
<span class="label">能见度:</span>
|
||||||
|
<span class="value">{{ weatherInfo.vis || '--' }}km</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="header-time">{{ currentTime }}</div>
|
||||||
|
<div class="notice-marquee" v-if="noticeList.length > 0">
|
||||||
|
<div class="marquee-content" :class="{ 'marquee-scroll': noticeList.length > 1 }">
|
||||||
|
<div class="notice-item" v-for="(item, index) in displayNoticeList" :key="index">
|
||||||
|
{{ item }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted, onUnmounted, computed } from 'vue'
|
||||||
|
import server from '@/utils/service'
|
||||||
|
|
||||||
|
const currentTime = ref('')
|
||||||
|
const weatherInfo = ref({})
|
||||||
|
const noticeList = ref([])
|
||||||
|
const currentNoticeIndex = ref(0)
|
||||||
|
let noticeTimer = null
|
||||||
|
let timeTimer = null
|
||||||
|
|
||||||
|
const displayNoticeList = computed(() => {
|
||||||
|
if (noticeList.value.length === 0) return []
|
||||||
|
return [noticeList.value[currentNoticeIndex.value]]
|
||||||
|
})
|
||||||
|
|
||||||
|
const updateTime = () => {
|
||||||
|
const now = new Date()
|
||||||
|
currentTime.value = now.toLocaleString('zh-CN', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
}).replace(/\//g, '-')
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchWeather = async () => {
|
||||||
|
try {
|
||||||
|
const response = await server.getQxjGdybHour()
|
||||||
|
if (response.data.success) {
|
||||||
|
weatherInfo.value = response.data.data.tq
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取天气信息失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchTaskNotice = async () => {
|
||||||
|
try {
|
||||||
|
const response = await server.getTaskNotice()
|
||||||
|
console.log('原始数据-走马灯:', response.data.data)
|
||||||
|
if (response.data.success) {
|
||||||
|
const data = response.data.data
|
||||||
|
if (data && data.content) {
|
||||||
|
const textContent = data.content.replace(/<[^>]+>/g, '')
|
||||||
|
noticeList.value = [textContent]
|
||||||
|
startNoticeScroll()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取走马灯数据失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const startNoticeScroll = () => {
|
||||||
|
if (noticeList.value.length <= 1) return
|
||||||
|
|
||||||
|
noticeTimer = setInterval(() => {
|
||||||
|
currentNoticeIndex.value = (currentNoticeIndex.value + 1) % noticeList.value.length
|
||||||
|
}, 3000)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
updateTime()
|
||||||
|
fetchWeather()
|
||||||
|
fetchTaskNotice()
|
||||||
|
timeTimer = setInterval(updateTime, 1000)
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
clearInterval(timeTimer)
|
||||||
|
clearInterval(noticeTimer)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@font-face {
|
||||||
|
font-family: BlackOpsOne;
|
||||||
|
src: url('/fonts/BlackOpsOne-Regular.ttf') format('truetype');
|
||||||
|
font-weight: 400;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-header {
|
||||||
|
width: 4800px;
|
||||||
|
height: 106px;
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: url('/imgs/top.png') no-repeat center center;
|
||||||
|
background-size: cover;
|
||||||
|
// border-bottom: 2px solid rgba(0, 212, 255, 0.5);
|
||||||
|
|
||||||
|
.header-title {
|
||||||
|
.title-img {
|
||||||
|
max-height: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-time {
|
||||||
|
position: absolute;
|
||||||
|
// right: 40px;
|
||||||
|
top: 110px;
|
||||||
|
left: 50%;
|
||||||
|
font-family: BlackOpsOne, sans-serif;
|
||||||
|
font-size: 44px;
|
||||||
|
color: #19EBFF;
|
||||||
|
text-align: center;
|
||||||
|
// 居中显示
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notice-marquee {
|
||||||
|
position: absolute;
|
||||||
|
right: 40px;
|
||||||
|
top: 55px;
|
||||||
|
width: 1300px;
|
||||||
|
height: 45px;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.marquee-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.notice-item {
|
||||||
|
font-size: 40px;
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
white-space: nowrap;
|
||||||
|
animation: marquee 20s linear infinite;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes marquee {
|
||||||
|
0% {
|
||||||
|
transform: translateX(100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: translateX(-100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.weather-info {
|
||||||
|
position: absolute;
|
||||||
|
left: 200px;
|
||||||
|
top: 70%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
|
||||||
|
.weather-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
line-height: 45px;
|
||||||
|
|
||||||
|
.label {
|
||||||
|
font-size: 36px;
|
||||||
|
color: #FFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
font-size: 36px;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,280 @@
|
||||||
|
<template>
|
||||||
|
<div class="module-eight">
|
||||||
|
<div class="module-body">
|
||||||
|
<div class="time-range">(2026-05-20)</div>
|
||||||
|
<div class="liziqiu-wrapper">
|
||||||
|
<img class="liziqiu" src="/imgs/liziqiu.png" alt="liziqiu" />
|
||||||
|
</div>
|
||||||
|
<div class="center-content">
|
||||||
|
<div class="center-label">施工总数</div>
|
||||||
|
<div class="center-number">{{ totalData.value }}</div>
|
||||||
|
</div>
|
||||||
|
<div v-for="(item, index) in otherData" :key="index" class="circle-item" :style="item.style">
|
||||||
|
<div class="circle-label">{{ item.label }}</div>
|
||||||
|
<div class="circle-number">{{ item.value }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="table-container">
|
||||||
|
<div class="table-header">
|
||||||
|
<div class="header-item">线路</div>
|
||||||
|
<div class="header-item">施工总数</div>
|
||||||
|
<div class="header-item">跨线</div>
|
||||||
|
<div class="header-item">重大</div>
|
||||||
|
<div class="header-item">动车</div>
|
||||||
|
<div class="header-item">停电</div>
|
||||||
|
<div class="header-item">过夜车</div>
|
||||||
|
<div class="header-item">正线留车</div>
|
||||||
|
<div class="header-item">施工关联</div>
|
||||||
|
<div class="header-item">突破天窗</div>
|
||||||
|
</div>
|
||||||
|
<div class="table-body">
|
||||||
|
<div v-for="(line, index) in tableData" :key="index" class="table-row">
|
||||||
|
<div class="line-name"
|
||||||
|
:style="{ backgroundColor: getLineBgColor(line.line), color: getLineTextColor(line.line) }">{{ line.line
|
||||||
|
}}</div>
|
||||||
|
<div class="row-item">{{ line.total }}</div>
|
||||||
|
<div class="row-item">{{ line.kuaxian }}</div>
|
||||||
|
<div class="row-item">{{ line.zhongda }}</div>
|
||||||
|
<div class="row-item">{{ line.dongche }}</div>
|
||||||
|
<div class="row-item">{{ line.tingdian }}</div>
|
||||||
|
<div class="row-item">{{ line.guoyeche }}</div>
|
||||||
|
<div class="row-item">{{ line.zhengxian }}</div>
|
||||||
|
<div class="row-item">{{ line.shigong }}</div>
|
||||||
|
<div class="row-item">{{ line.tupo }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
const totalData = ref({
|
||||||
|
label: '施工总数',
|
||||||
|
value: 51
|
||||||
|
})
|
||||||
|
|
||||||
|
const otherData = ref([
|
||||||
|
{ label: '跨线', value: 2, style: { top: '10%', left: '6%' } },
|
||||||
|
{ label: '重大', value: 2, style: { top: '28%', left: '12%' } },
|
||||||
|
{ label: '动车', value: 3, style: { top: '15%', right: '28%' } },
|
||||||
|
{ label: '停电', value: 2, style: { top: '27%', right: '14%' } },
|
||||||
|
{ label: '过夜车', value: 3, style: { top: '45%', right: '5%' } },
|
||||||
|
{ label: '正线留车', value: 0, style: { top: '12%', right: '4%' } },
|
||||||
|
{ label: '施工关联', value: 0, style: { top: '48%', left: '6%' } },
|
||||||
|
{ label: '突破天窗', value: 0, style: { top: '14%', left: '32%' } }
|
||||||
|
])
|
||||||
|
|
||||||
|
const tableData = ref([
|
||||||
|
{ line: '3 号线', color: '#FBD61D', total: 1, kuaxian: 0, zhongda: 1, dongche: 0, tingdian: 0, guoyeche: 0, zhengxian: 0, shigong: 0, tupo: 0 },
|
||||||
|
{ line: '4 号线', color: '#451E83', total: 10, kuaxian: 1, zhongda: 1, dongche: 2, tingdian: 0, guoyeche: 0, zhengxian: 0, shigong: 0, tupo: 0 },
|
||||||
|
{ line: '7 号线', color: '#ED7010', total: 38, kuaxian: 1, zhongda: 0, dongche: 0, tingdian: 2, guoyeche: 3, zhengxian: 0, shigong: 0, tupo: 0 },
|
||||||
|
{ line: '15 号线', color: '#C4AF8C', total: 2, kuaxian: 0, zhongda: 0, dongche: 1, tingdian: 0, guoyeche: 0, zhengxian: 0, shigong: 0, tupo: 0 }
|
||||||
|
])
|
||||||
|
|
||||||
|
const getLineBgColor = (lineName) => {
|
||||||
|
const colors = {
|
||||||
|
3: '#FBD61D',
|
||||||
|
4: '#451E83',
|
||||||
|
7: '#ED7010',
|
||||||
|
15: '#C4AF8C'
|
||||||
|
}
|
||||||
|
const match = lineName.match(/\d+/)
|
||||||
|
const number = match ? parseInt(match[0]) : 0
|
||||||
|
return colors[number] || '#ffcc00'
|
||||||
|
}
|
||||||
|
|
||||||
|
const getLineTextColor = (lineName) => {
|
||||||
|
const textColors = {
|
||||||
|
4: '#ffffff'
|
||||||
|
}
|
||||||
|
const match = lineName.match(/\d+/)
|
||||||
|
const number = match ? parseInt(match[0]) : 0
|
||||||
|
return textColors[number] || '#000000'
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.module-eight {
|
||||||
|
width: 892px;
|
||||||
|
height: 880px;
|
||||||
|
position: absolute;
|
||||||
|
left: 2948px;
|
||||||
|
top: 610px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: url('/imgs/meightbg.png') no-repeat center center;
|
||||||
|
background-size: cover;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
|
||||||
|
.module-body {
|
||||||
|
flex: 1;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.time-range {
|
||||||
|
position: absolute;
|
||||||
|
top: 8px;
|
||||||
|
left: 160px;
|
||||||
|
font-family: Alibaba PuHuiTi 3.0;
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: normal;
|
||||||
|
letter-spacing: 0px;
|
||||||
|
font-variation-settings: "opsz" auto;
|
||||||
|
color: #FFFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.liziqiu-wrapper {
|
||||||
|
position: absolute;
|
||||||
|
top: 40%;
|
||||||
|
left: 49%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
animation: rotate 10s linear infinite;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
.liziqiu {
|
||||||
|
width: 300px;
|
||||||
|
height: 300px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.center-content {
|
||||||
|
position: absolute;
|
||||||
|
top: 40%;
|
||||||
|
left: 49%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
text-align: center;
|
||||||
|
z-index: 10;
|
||||||
|
|
||||||
|
.center-label {
|
||||||
|
font-family: Alibaba PuHuiTi 3.0;
|
||||||
|
font-size: 42px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: normal;
|
||||||
|
letter-spacing: 0px;
|
||||||
|
font-variation-settings: "opsz" auto;
|
||||||
|
color: white;
|
||||||
|
text-shadow: 0px 0px 10px rgba(30, 121, 255, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.center-number {
|
||||||
|
font-family: Alibaba PuHuiTi 3.0;
|
||||||
|
font-size: 50px;
|
||||||
|
font-weight: bold;
|
||||||
|
line-height: normal;
|
||||||
|
letter-spacing: 0px;
|
||||||
|
font-variation-settings: "opsz" auto;
|
||||||
|
color: #FFFFFF;
|
||||||
|
text-shadow: 0px 1px 4px rgba(7, 44, 137, 0.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.circle-item {
|
||||||
|
position: absolute;
|
||||||
|
width: 100px;
|
||||||
|
height: 80px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 10;
|
||||||
|
|
||||||
|
.circle-label {
|
||||||
|
font-family: Alibaba PuHuiTi 3.0;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 24px;
|
||||||
|
text-align: center;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
letter-spacing: 0px;
|
||||||
|
font-variation-settings: "opsz" auto;
|
||||||
|
color: #FFFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.circle-number {
|
||||||
|
font-family: Alibaba PuHuiTi 3.0;
|
||||||
|
font-size: 42px;
|
||||||
|
font-weight: bold;
|
||||||
|
line-height: normal;
|
||||||
|
letter-spacing: 0px;
|
||||||
|
font-variation-settings: "opsz" auto;
|
||||||
|
color: #FFFFFF;
|
||||||
|
text-shadow: 0px 1px 4px rgba(7, 44, 137, 0.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-container {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 20px;
|
||||||
|
left: 20px;
|
||||||
|
right: 20px;
|
||||||
|
z-index: 10;
|
||||||
|
|
||||||
|
.table-header {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(10, 1fr);
|
||||||
|
gap: 8px;
|
||||||
|
padding: 10px;
|
||||||
|
background: rgba(0, 212, 255, 0.1);
|
||||||
|
border: 1px solid rgba(0, 212, 255, 0.3);
|
||||||
|
border-radius: 4px;
|
||||||
|
|
||||||
|
.header-item {
|
||||||
|
font-family: Alibaba PuHuiTi 3.0;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #00d4ff;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 8px;
|
||||||
|
|
||||||
|
.table-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(10, 1fr);
|
||||||
|
gap: 8px;
|
||||||
|
padding: 12px;
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border: 1px solid rgba(0, 212, 255, 0.2);
|
||||||
|
border-radius: 4px;
|
||||||
|
|
||||||
|
.row-item {
|
||||||
|
font-family: Alibaba PuHuiTi 3.0;
|
||||||
|
font-size: 16px;
|
||||||
|
color: #fff;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-name {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 16px;
|
||||||
|
// padding: 4px 1px;
|
||||||
|
border-radius: 4px;
|
||||||
|
display: inline-block;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes rotate {
|
||||||
|
from {
|
||||||
|
transform: translate(-50%, -50%) rotate(0deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
transform: translate(-50%, -50%) rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,145 @@
|
||||||
|
<template>
|
||||||
|
<div class="module-five">
|
||||||
|
<!-- <div class="module-title">模块五</div> -->
|
||||||
|
<div class="module-body">
|
||||||
|
<div class="indicator-row">
|
||||||
|
<div v-if="topLeft" class="indicator-item indicator-item-top">
|
||||||
|
<div class="indicator-name">{{ topLeft.baseline }}</div>
|
||||||
|
<div class="indicator-value">{{ topLeft.challenge }}</div>
|
||||||
|
<div class="indicator-value">{{ topLeft.actual }}</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="topRight" class="indicator-item indicator-item-top1">
|
||||||
|
<div class="indicator-name">{{ topRight.baseline }}</div>
|
||||||
|
<div class="indicator-value">{{ topRight.challenge }}</div>
|
||||||
|
<div class="indicator-value">{{ topRight.actual }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="indicator-row">
|
||||||
|
<div v-if="bottomLeft" class="indicator-item indicator-item-bottom">
|
||||||
|
<div class="indicator-name">{{ bottomLeft.baseline }}</div>
|
||||||
|
<div class="indicator-value">{{ bottomLeft.challenge }}</div>
|
||||||
|
<div class="indicator-value">{{ bottomLeft.actual }}</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="bottomRight" class="indicator-item indicator-item-bottom1">
|
||||||
|
<div class="indicator-name">{{ bottomRight.baseline }}</div>
|
||||||
|
<div class="indicator-value">{{ bottomRight.challenge }}</div>
|
||||||
|
<div class="indicator-value">{{ bottomRight.actual }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, onMounted } from 'vue'
|
||||||
|
import server from '@/utils/service'
|
||||||
|
|
||||||
|
const indicatorData = ref([])
|
||||||
|
|
||||||
|
const fetchIndicator = async () => {
|
||||||
|
try {
|
||||||
|
const response = await server.getIndicator()
|
||||||
|
if (response.data.success) {
|
||||||
|
indicatorData.value = response.data.data
|
||||||
|
console.log('指标数据:', indicatorData.value)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取指标数据失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const topLeft = computed(() => indicatorData.value.find(item => item.sort === '1'))
|
||||||
|
const topRight = computed(() => indicatorData.value.find(item => item.sort === '2'))
|
||||||
|
const bottomLeft = computed(() => indicatorData.value.find(item => item.sort === '3'))
|
||||||
|
const bottomRight = computed(() => indicatorData.value.find(item => item.sort === '4'))
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchIndicator()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.module-five {
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
left: 3850px;
|
||||||
|
top: 33px;
|
||||||
|
width: 930px;
|
||||||
|
height: 549px;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: url('/imgs/mfivebg1.png') no-repeat center center;
|
||||||
|
background-size: cover;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
|
||||||
|
.module-title {
|
||||||
|
height: 40px;
|
||||||
|
line-height: 40px;
|
||||||
|
padding: 0 16px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #00d4ff;
|
||||||
|
background: rgba(0, 212, 255, 0.1);
|
||||||
|
border-bottom: 1px solid rgba(0, 212, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-body {
|
||||||
|
flex: 1;
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-around;
|
||||||
|
// padding: 20px;
|
||||||
|
|
||||||
|
.indicator-row {
|
||||||
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
|
// justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.indicator-item {
|
||||||
|
position: absolute;
|
||||||
|
flex: 1;
|
||||||
|
margin-left: 210px;
|
||||||
|
font-family: YouSheBiaoTiHei;
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: normal;
|
||||||
|
line-height: normal;
|
||||||
|
letter-spacing: 0em;
|
||||||
|
color: #fff;
|
||||||
|
font-style: italic;
|
||||||
|
|
||||||
|
.indicator-name {
|
||||||
|
position: relative;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.indicator-value {
|
||||||
|
position: relative;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.indicator-item-top {
|
||||||
|
margin-top: 155px;
|
||||||
|
left: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.indicator-item-top1 {
|
||||||
|
margin-top: 155px;
|
||||||
|
left: 515px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.indicator-item-bottom {
|
||||||
|
margin-top: 110px;
|
||||||
|
left: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.indicator-item-bottom1 {
|
||||||
|
margin-top: 110px;
|
||||||
|
left: 515px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,427 @@
|
||||||
|
<template>
|
||||||
|
<div class="module-four">
|
||||||
|
<!-- <div class="module-title">模块四</div> -->
|
||||||
|
<div class="module-body">
|
||||||
|
<div class="scroll-wrapper">
|
||||||
|
<div class="scroll-list">
|
||||||
|
<div class="list-item" v-for="(item, index) in stationData" :key="item.id + '-' + index">
|
||||||
|
<div class="cell-bg">
|
||||||
|
<div class="progress-bar">
|
||||||
|
<div class="progress-fill" :style="{ width: item.progress + '%' }"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="rank" :class="'rank-' + ((index % stationData.length) + 1)">{{ String(item.rank).padStart(2,
|
||||||
|
'0')
|
||||||
|
}}</div>
|
||||||
|
<div class="line-numbers">
|
||||||
|
<span v-for="lineId in getLineIdList(item.lineId)" :key="lineId" class="line-number-badge"
|
||||||
|
:style="getLineNumberStyle(lineId)">
|
||||||
|
{{ lineId }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="station-name">{{ item.stationName }}</div>
|
||||||
|
<div class="action-type">{{ item.actionType }}</div>
|
||||||
|
<!-- <div class="progress-bar">
|
||||||
|
<div class="progress-fill" :style="{ width: item.progress + '%' }"></div>
|
||||||
|
</div> -->
|
||||||
|
<div class="in-count">
|
||||||
|
<span class="highlight">{{ item.inCount }}</span>
|
||||||
|
<span class="unit">人</span>
|
||||||
|
</div>
|
||||||
|
<div class="out-count">
|
||||||
|
<span :style="getOutCountColorStyle(item)">{{ getOutCountPercentage(item) }}</span>
|
||||||
|
<span class="unit">%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||||
|
import server from '@/utils/service'
|
||||||
|
|
||||||
|
const stationData = ref([])
|
||||||
|
|
||||||
|
const fetchStationMaxList = async () => {
|
||||||
|
try {
|
||||||
|
const response = await server.getStationMaxList()
|
||||||
|
if (response.data.success) {
|
||||||
|
// console.log('原始最大客流列表:', response.data.data)
|
||||||
|
const maxTotal = Math.max(...response.data.data.map(item => parseFloat(item.total)))
|
||||||
|
stationData.value = response.data.data.map((item, index) => ({
|
||||||
|
id: item.stationId,
|
||||||
|
rank: index + 1,
|
||||||
|
stationName: item.stationName,
|
||||||
|
actionType: '进出站',
|
||||||
|
progress: Math.round((parseFloat(item.total) / maxTotal) * 100),
|
||||||
|
inCount: item.total,
|
||||||
|
outCount: item.compareTotal,
|
||||||
|
lineId: item.lineId
|
||||||
|
}))
|
||||||
|
console.log('站点最大客流列表:', stationData.value)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取站点最大客流列表失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentIndex = ref(0)
|
||||||
|
const transitionState = ref(true)
|
||||||
|
let timer = null
|
||||||
|
const itemHeight = 70
|
||||||
|
const visibleCount = 3
|
||||||
|
|
||||||
|
const displayList = computed(() => {
|
||||||
|
return stationData.value
|
||||||
|
})
|
||||||
|
|
||||||
|
const getLineIdList = (lineId) => {
|
||||||
|
if (!lineId) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
return String(lineId).split(',').map(id => id.trim())
|
||||||
|
}
|
||||||
|
|
||||||
|
const getLineNumberStyle = (lineId) => {
|
||||||
|
const lineNum = parseInt(lineId)
|
||||||
|
const colors = {
|
||||||
|
1: '#E6002A',
|
||||||
|
2: '#8CC220',
|
||||||
|
3: '#FBD61D',
|
||||||
|
4: '#451E83',
|
||||||
|
5: '#944D9A',
|
||||||
|
6: '#D40068',
|
||||||
|
7: '#ED7010',
|
||||||
|
8: '#0095D9',
|
||||||
|
9: '#87CAED',
|
||||||
|
10: '#C6AFD4',
|
||||||
|
11: '#871C2B',
|
||||||
|
12: '#007A60',
|
||||||
|
13: '#E999C0',
|
||||||
|
14: '#9A982F',
|
||||||
|
15: '#C4AF8C',
|
||||||
|
16: '#98D1C0',
|
||||||
|
17: '#B6766C',
|
||||||
|
18: '#C09453'
|
||||||
|
}
|
||||||
|
const textColors = {
|
||||||
|
4: '#ffffff'
|
||||||
|
}
|
||||||
|
const bgColor = colors[lineNum] || '#ffcc00'
|
||||||
|
const textColor = textColors[lineNum] || '#000000'
|
||||||
|
return {
|
||||||
|
backgroundColor: bgColor,
|
||||||
|
color: textColor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getOutCountPercentage = (item) => {
|
||||||
|
if (!item.outCount || item.outCount === 0) {
|
||||||
|
return '0.00'
|
||||||
|
}
|
||||||
|
const percentage = ((item.inCount - item.outCount) / item.inCount) * 100
|
||||||
|
// return Math.abs(percentage).toFixed(2)
|
||||||
|
return percentage.toFixed(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getOutCountColorStyle = (item) => {
|
||||||
|
if (!item.outCount || item.outCount === 0) {
|
||||||
|
return { color: '#ffffff', fontWeight: 'bold' }
|
||||||
|
}
|
||||||
|
const percentage = (item.inCount - item.outCount) / item.inCount
|
||||||
|
if (percentage > 0) {
|
||||||
|
return { color: '#ff3333', fontWeight: 'bold' }
|
||||||
|
} else if (percentage < 0) {
|
||||||
|
return { color: '#00ff00', fontWeight: 'bold' }
|
||||||
|
} else {
|
||||||
|
return { color: '#ffffff', fontWeight: 'bold' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const startScroll = () => {
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleScrollEnd = (index) => {
|
||||||
|
console.log('滚动到第', index, '项')
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchStationMaxList()
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (timer) {
|
||||||
|
clearInterval(timer)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.module-four {
|
||||||
|
width: 787.46px;
|
||||||
|
height: 894px;
|
||||||
|
position: absolute;
|
||||||
|
left: 892px;
|
||||||
|
top: 33px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: url('/imgs/mfourbg.png') no-repeat center center;
|
||||||
|
background-size: cover;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
|
||||||
|
.module-title {
|
||||||
|
height: 40px;
|
||||||
|
line-height: 40px;
|
||||||
|
padding: 0 16px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #00d4ff;
|
||||||
|
background: rgba(0, 212, 255, 0.1);
|
||||||
|
border-bottom: 1px solid rgba(0, 212, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-body {
|
||||||
|
flex: 1;
|
||||||
|
padding: 20px;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.scroll-wrapper {
|
||||||
|
overflow: visible;
|
||||||
|
margin-top: 100px;
|
||||||
|
// background: yellow;
|
||||||
|
|
||||||
|
.scroll-list {
|
||||||
|
.list-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 70px;
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
background: transparent;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
|
||||||
|
.cell-bg {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
width: 742.6px;
|
||||||
|
height: 44px;
|
||||||
|
background: url('/imgs/cell.png') no-repeat center center;
|
||||||
|
background-size: 100% 100%;
|
||||||
|
z-index: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rank {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
width: 40px;
|
||||||
|
height: 28px;
|
||||||
|
line-height: 28px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-right: 16px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-left: 8px;
|
||||||
|
|
||||||
|
// 10个等级
|
||||||
|
&.rank-1 {
|
||||||
|
background: linear-gradient(90deg, #00FFFF 0%, #00FFFF 100%);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.rank-2 {
|
||||||
|
background: linear-gradient(90deg, #00FFFF 0%, #00FFFF 100%);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.rank-3 {
|
||||||
|
background: linear-gradient(90deg, #00FFFF 0%, #00FFFF 100%);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.rank-4 {
|
||||||
|
background: linear-gradient(90deg, #fff 0%, #fff 100%);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.rank-5 {
|
||||||
|
background: linear-gradient(90deg, #fff 0%, #fff 100%);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.rank-6 {
|
||||||
|
background: linear-gradient(90deg, #fff 0%, #fff 100%);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.rank-7 {
|
||||||
|
background: linear-gradient(90deg, #fff 0%, #fff 100%);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.rank-8 {
|
||||||
|
background: linear-gradient(90deg, #fff 0%, #fff 100%);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.rank-9 {
|
||||||
|
background: linear-gradient(90deg, #fff 0%, #fff 100%);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.rank-10 {
|
||||||
|
background: linear-gradient(90deg, #fff 0%, #fff 100%);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-numbers {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2px;
|
||||||
|
width: 70px;
|
||||||
|
// margin-left: 10px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
// background-color: yellow;
|
||||||
|
|
||||||
|
.line-number-badge {
|
||||||
|
width: 20px;
|
||||||
|
height: 30px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 700;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.station-name {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
width: 140px;
|
||||||
|
font-size: 23px;
|
||||||
|
color: #fff;
|
||||||
|
flex-shrink: 0;
|
||||||
|
// margin-left: 20px;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-type {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
width: 90px;
|
||||||
|
font-size: 23px;
|
||||||
|
color: #fff;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-left: 76px;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
flex: 1;
|
||||||
|
height: 6px;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 3px;
|
||||||
|
margin-left: 60px;
|
||||||
|
margin-top: 37px;
|
||||||
|
overflow: hidden;
|
||||||
|
width: 500px;
|
||||||
|
|
||||||
|
.progress-fill {
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(270deg, #00FFFF 0%, rgba(0, 183, 255, 0.18) 100%);
|
||||||
|
border-radius: 3px;
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.in-count {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
width: 150px;
|
||||||
|
text-align: left;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #47ffff;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-left: 50px;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
// background-color: yellow;
|
||||||
|
|
||||||
|
.highlight {
|
||||||
|
color: #47ffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unit {
|
||||||
|
font-size: 10px;
|
||||||
|
color: #47ffff;
|
||||||
|
margin-left: 2px;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.out-count {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
width: 120px;
|
||||||
|
text-align: left;
|
||||||
|
font-size: 16px;
|
||||||
|
color: #FFFFFF;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-left: 0px;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
|
||||||
|
.unit {
|
||||||
|
font-size: 10px;
|
||||||
|
color: #FFFFFF;
|
||||||
|
margin-left: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,292 @@
|
||||||
|
<template>
|
||||||
|
<div class="module-nine">
|
||||||
|
<div class="module-body">
|
||||||
|
<div class="yuqing-container">
|
||||||
|
<div class="yuqing">
|
||||||
|
<!-- <div class="time">{{ time }}</div> -->
|
||||||
|
<div class="content">
|
||||||
|
<div class="columns">
|
||||||
|
<div class="column left">
|
||||||
|
<div v-for="item in leftList" :key="item.id" class="list-item">
|
||||||
|
<div class="newsNum">{{ item.originalIndex + 1 }}</div>
|
||||||
|
<div class="newsTitle">{{ item.newsTitle }}</div>
|
||||||
|
<div class="line"></div>
|
||||||
|
<div class="meta">时间:{{ item.issuePosttime }}</div>
|
||||||
|
<div class="meta1"><span>热度</span><span style="font-weight: bold; margin-left: 120px;"> {{
|
||||||
|
item.simCount }}</span></div>
|
||||||
|
<div class="zdItem" @click.stop="toggleStatus(item)">{{ item.status === '0' ? '置顶' : '取消' }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="column right">
|
||||||
|
<div v-for="item in rightList" :key="item.id" class="list-item">
|
||||||
|
<div class="newsNum">{{ item.originalIndex + 1 }}</div>
|
||||||
|
<div class="newsTitle">{{ item.newsTitle }}</div>
|
||||||
|
<div class="line"></div>
|
||||||
|
<div class="meta">时间:{{ item.issuePosttime }}</div>
|
||||||
|
<div class="meta1">热度 <span style="font-weight: bold; margin-left: 120px;">{{ item.simCount }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="zdItem" @click.stop="toggleStatus(item)">{{ item.status === '0' ? '置顶' : '取消' }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, onMounted } from 'vue'
|
||||||
|
|
||||||
|
const time = ref(new Date().toLocaleString())
|
||||||
|
setInterval(() => time.value = new Date().toLocaleString(), 1000)
|
||||||
|
|
||||||
|
const list = ref([
|
||||||
|
{ id: 1, newsTitle: '上海地铁1号线早高峰客流超负荷 运营方加开临客', issuePosttime: '2026-05-20 08:15:23', simCount: '1234', newsUrl: 'http://example.com/news1', status: '0' },
|
||||||
|
{ id: 2, newsTitle: '地铁2号线发生列车延误事件 乘客有序疏散', issuePosttime: '2026-05-20 09:30:45', simCount: '892', newsUrl: 'http://example.com/news2', status: '1' },
|
||||||
|
{ id: 3, newsTitle: '地铁10号线今日首班车提前发车', issuePosttime: '2026-05-20 06:45:12', simCount: '567', newsUrl: 'http://example.com/news3', status: '0' },
|
||||||
|
{ id: 4, newsTitle: '地铁13号线新站点今日正式启用', issuePosttime: '2026-05-20 10:20:33', simCount: '789', newsUrl: 'http://example.com/news4', status: '1' },
|
||||||
|
{ id: 5, newsTitle: '地铁9号线晚高峰客流统计发布', issuePosttime: '2026-05-20 18:45:56', simCount: '2345', newsUrl: 'http://example.com/news5', status: '0' },
|
||||||
|
{ id: 6, newsTitle: '地铁4号线列车检修工作顺利完成', issuePosttime: '2026-05-20 14:30:18', simCount: '456', newsUrl: 'http://example.com/news6', status: '1' },
|
||||||
|
{ id: 7, newsTitle: '地铁8号线今日运营数据亮眼', issuePosttime: '2026-05-20 16:22:44', simCount: '1567', newsUrl: 'http://example.com/news7', status: '0' },
|
||||||
|
{ id: 8, newsTitle: '地铁7号线突发设备故障 已及时修复', issuePosttime: '2026-05-20 11:15:29', simCount: '678', newsUrl: 'http://example.com/news8', status: '1' },
|
||||||
|
{ id: 9, newsTitle: '地铁16号线新线路规划公布', issuePosttime: '2026-05-20 13:40:55', simCount: '890', newsUrl: 'http://example.com/news9', status: '0' },
|
||||||
|
{ id: 10, newsTitle: '地铁5号线早高峰运营情况良好', issuePosttime: '2026-05-20 07:55:33', simCount: '1123', newsUrl: 'http://example.com/news10', status: '1' }
|
||||||
|
])
|
||||||
|
|
||||||
|
const processedList = computed(() => list.value.map((item, index) => ({ ...item, originalIndex: index })))
|
||||||
|
const leftList = computed(() => processedList.value.filter((_, i) => i % 2 === 0))
|
||||||
|
const rightList = computed(() => processedList.value.filter((_, i) => i % 2 === 1))
|
||||||
|
|
||||||
|
const fetchData = () => {
|
||||||
|
// 模拟数据,不调用接口
|
||||||
|
console.log('模拟数据加载完成')
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchData()
|
||||||
|
setTimeout(fetchData, 1000 * 60 * 5)
|
||||||
|
})
|
||||||
|
|
||||||
|
const goUrl = (item) => window.open(item.newsUrl)
|
||||||
|
|
||||||
|
const toggleStatus = (item) => {
|
||||||
|
const newStatus = item.status === '0' ? '1' : '0'
|
||||||
|
item.status = newStatus
|
||||||
|
console.log('更新状态:', item.id, newStatus)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.module-nine {
|
||||||
|
width: 934px;
|
||||||
|
height: 893.14px;
|
||||||
|
position: absolute;
|
||||||
|
left: 3850px;
|
||||||
|
top: 610px;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: url('/imgs/mninebg.png') no-repeat center center;
|
||||||
|
background-size: cover;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
|
||||||
|
.module-body {
|
||||||
|
flex: 1;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.yuqing-container {
|
||||||
|
position: absolute;
|
||||||
|
// left: 23px;
|
||||||
|
// top: 52px;
|
||||||
|
width: 97.48%;
|
||||||
|
height: 89.82%;
|
||||||
|
|
||||||
|
.yuqing {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
// background-image: url('/imgs/yuqing/yuqingBg.png');
|
||||||
|
background-size: cover;
|
||||||
|
position: relative;
|
||||||
|
// background: yellow;
|
||||||
|
|
||||||
|
.time {
|
||||||
|
position: absolute;
|
||||||
|
top: 3px;
|
||||||
|
left: 35px;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
position: absolute;
|
||||||
|
top: 60px;
|
||||||
|
left: 20px;
|
||||||
|
width: 944px;
|
||||||
|
height: 810px;
|
||||||
|
overflow-y: auto;
|
||||||
|
scrollbar-width: none;
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.columns {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.column {
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
.list-item {
|
||||||
|
position: relative;
|
||||||
|
width: 410px;
|
||||||
|
height: 185px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
background-size: contain;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 16px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
// background: linear-gradient(90deg, rgba(36, 186, 255, 0.12) 0%, rgba(36, 186, 255, 0) 100%);
|
||||||
|
// background-color: yellow;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.left .list-item {
|
||||||
|
background-image: url('/imgs/itembg.png');
|
||||||
|
}
|
||||||
|
|
||||||
|
.right .list-item {
|
||||||
|
background-image: url('/imgs/itembg.png');
|
||||||
|
}
|
||||||
|
|
||||||
|
.newsNum {
|
||||||
|
padding-top: 6px;
|
||||||
|
position: absolute;
|
||||||
|
top: 14px;
|
||||||
|
left: 20px;
|
||||||
|
color: red;
|
||||||
|
font-size: 14px;
|
||||||
|
width: 27px;
|
||||||
|
height: 32px;
|
||||||
|
text-align: center;
|
||||||
|
background-image: url('/imgs/hot.png');
|
||||||
|
background-size: contain;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center;
|
||||||
|
line-height: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.newsTitle {
|
||||||
|
position: absolute;
|
||||||
|
top: 20px;
|
||||||
|
left: 60px;
|
||||||
|
width: 320px;
|
||||||
|
font-weight: bold;
|
||||||
|
line-height: 20px;
|
||||||
|
display: -webkit-box;
|
||||||
|
// -webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
.line {
|
||||||
|
position: absolute;
|
||||||
|
top: 74px;
|
||||||
|
left: 20px;
|
||||||
|
width: 375px;
|
||||||
|
height: 2px;
|
||||||
|
// background-color: #415C71;
|
||||||
|
border: 1px solid #5F83A0;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.left .newsTitle {
|
||||||
|
left: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right .newsTitle {
|
||||||
|
left: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta {
|
||||||
|
position: absolute;
|
||||||
|
color: #A4A4A4;
|
||||||
|
font-size: 18px;
|
||||||
|
margin-left: 20px;
|
||||||
|
line-height: 24px;
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta1 {
|
||||||
|
margin-top: 80px;
|
||||||
|
position: absolute;
|
||||||
|
color: #E0E0E0;
|
||||||
|
font-size: 18px;
|
||||||
|
margin-left: 20px;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left .meta:nth-of-type(1) {
|
||||||
|
top: 105px;
|
||||||
|
left: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left .meta:nth-of-type(2) {
|
||||||
|
top: 105px;
|
||||||
|
left: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right .meta:nth-of-type(1) {
|
||||||
|
top: 105px;
|
||||||
|
left: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right .meta:nth-of-type(2) {
|
||||||
|
top: 105px;
|
||||||
|
left: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zdItem {
|
||||||
|
position: absolute;
|
||||||
|
top: 120px;
|
||||||
|
left: 350px;
|
||||||
|
width: 60px;
|
||||||
|
height: 24px;
|
||||||
|
border: 1px solid #29F0FA;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
line-height: 24px;
|
||||||
|
text-align: center;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left .zdItem {
|
||||||
|
left: 320px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right .zdItem {
|
||||||
|
left: 320px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,199 @@
|
||||||
|
<template>
|
||||||
|
<div class="module-one">
|
||||||
|
<!-- <div class="module-title">模块一</div> -->
|
||||||
|
<div class="module-body">
|
||||||
|
<div class="pyramid-3d-wrapper">
|
||||||
|
<Pyramid3D :show-glow="true" :show-ring="true" glow-image="/imgs/guangyun.png" color="#00d4ff" :scale="1.77"
|
||||||
|
:rotation-speed="0.2" :auto-rotate="true" :width="252" :height="166" @loaded="onPyramidLoaded"
|
||||||
|
@error="onPyramidError" />
|
||||||
|
</div>
|
||||||
|
<div class="pyramid-3d-wrapper2">
|
||||||
|
<Pyramid3D :show-glow="true" :show-ring="true" glow-image="/imgs/guangyun.png" color="#00d4ff" :scale="1.77"
|
||||||
|
:rotation-speed="0.2" :auto-rotate="true" :width="252" :height="166" @loaded="onPyramidLoaded"
|
||||||
|
@error="onPyramidError" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<img class="img1" src="/imgs/Group630.png" alt="">
|
||||||
|
<img class="img2" src="/imgs/Group630.png" alt="">
|
||||||
|
<img class="img3" src="/imgs/Group630.png" alt="">
|
||||||
|
</div>
|
||||||
|
<div class="number-wrapper">
|
||||||
|
<span :style="{ color: levelColor }">{{ level }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="number-wrapper2">
|
||||||
|
<span>{{ days }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, onMounted } from 'vue'
|
||||||
|
import Pyramid3D from '@/components/Pyramid3D/index.vue'
|
||||||
|
import server from '@/utils/service'
|
||||||
|
|
||||||
|
const level = ref('')
|
||||||
|
const days = ref(0)
|
||||||
|
|
||||||
|
const levelColor = computed(() => {
|
||||||
|
const levelNum = Number(level.value)
|
||||||
|
if (levelNum === 1) return '#ff4d4f'
|
||||||
|
if (levelNum === 2) return '#faad14'
|
||||||
|
if (levelNum === 3) return '#ffffff'
|
||||||
|
return '#ffffff'
|
||||||
|
})
|
||||||
|
|
||||||
|
const fetchOperateInfo = async () => {
|
||||||
|
try {
|
||||||
|
const response = await server.getOperateInfo()
|
||||||
|
if (response.data.success) {
|
||||||
|
const { level: levelData, begindate } = response.data.data
|
||||||
|
level.value = levelData
|
||||||
|
|
||||||
|
const now = new Date()
|
||||||
|
const begin = new Date(begindate)
|
||||||
|
const diffTime = now.getTime() - begin.getTime()
|
||||||
|
const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24))
|
||||||
|
days.value = diffDays
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取运营信息失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onPyramidLoaded = () => {
|
||||||
|
console.log('Pyramid3D 加载完成')
|
||||||
|
}
|
||||||
|
|
||||||
|
const onPyramidError = (error) => {
|
||||||
|
console.error('Pyramid3D 加载失败:', error)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchOperateInfo()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.module-one {
|
||||||
|
width: 519px;
|
||||||
|
height: 281px;
|
||||||
|
position: absolute;
|
||||||
|
left: 17px;
|
||||||
|
top: 33px;
|
||||||
|
// border: 1px solid rgba(0, 212, 255, 0.3);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: rgba(4, 124, 235, 0.01);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
background: url('/imgs/monebg@2x.png') no-repeat center center;
|
||||||
|
background-size: cover;
|
||||||
|
|
||||||
|
.module-title {
|
||||||
|
height: 40px;
|
||||||
|
line-height: 40px;
|
||||||
|
padding: 0 16px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #00d4ff;
|
||||||
|
background: rgba(0, 212, 255, 0.1);
|
||||||
|
border-bottom: 1px solid rgba(0, 212, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-body {
|
||||||
|
flex: 1;
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
|
||||||
|
.pyramid-3d-wrapper {
|
||||||
|
position: absolute;
|
||||||
|
left: 5px;
|
||||||
|
top: 125px;
|
||||||
|
width: 252px;
|
||||||
|
height: 166px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pyramid-3d-wrapper2 {
|
||||||
|
position: absolute;
|
||||||
|
left: 255px;
|
||||||
|
top: 125px;
|
||||||
|
width: 252px;
|
||||||
|
height: 166px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.img1,
|
||||||
|
.img2,
|
||||||
|
.img3 {
|
||||||
|
position: absolute;
|
||||||
|
width: 52px;
|
||||||
|
height: 35px;
|
||||||
|
animation: float 1s ease-in-out infinite alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
.img1 {
|
||||||
|
left: 25px;
|
||||||
|
top: 215px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.img2 {
|
||||||
|
left: 195px;
|
||||||
|
top: 195px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.img3 {
|
||||||
|
left: 440px;
|
||||||
|
top: 235px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.number-wrapper {
|
||||||
|
position: absolute;
|
||||||
|
left: 90px;
|
||||||
|
top: 130px;
|
||||||
|
width: 90px;
|
||||||
|
height: 24px;
|
||||||
|
font-family: Alibaba PuHuiTi 3.0;
|
||||||
|
font-size: 52px;
|
||||||
|
font-weight: 800;
|
||||||
|
line-height: 24px;
|
||||||
|
text-align: center;
|
||||||
|
letter-spacing: 0px;
|
||||||
|
font-variation-settings: "opsz" auto;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.number-wrapper2 {
|
||||||
|
position: absolute;
|
||||||
|
left: 270px;
|
||||||
|
top: 130px;
|
||||||
|
width: 200px;
|
||||||
|
height: 52px;
|
||||||
|
opacity: 1;
|
||||||
|
font-family: Alibaba PuHuiTi 3.0;
|
||||||
|
font-size: 52px;
|
||||||
|
font-weight: 800;
|
||||||
|
line-height: 24px;
|
||||||
|
text-align: center;
|
||||||
|
letter-spacing: 0px;
|
||||||
|
|
||||||
|
font-variation-settings: "opsz" auto;
|
||||||
|
color: #fff;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes float {
|
||||||
|
0% {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,366 @@
|
||||||
|
<template>
|
||||||
|
<div class="module-seven">
|
||||||
|
<div class="module-body">
|
||||||
|
<div class="time-range">(2026-05-20)</div>
|
||||||
|
<div class="txt">
|
||||||
|
<div class="txt-item txt-item-1">
|
||||||
|
<img src="/imgs/type1.png" alt="">
|
||||||
|
<span class="txt-label">站台门</span><span class="txt-number">2</span>
|
||||||
|
</div>
|
||||||
|
<div class="txt-item txt-item-2">
|
||||||
|
<img src="/imgs/type2.png" alt="">
|
||||||
|
<span class="txt-label">火灾自动报警系统</span><span class="txt-number">16</span>
|
||||||
|
</div>
|
||||||
|
<div class="txt-item txt-item-3">
|
||||||
|
<img src="/imgs/type3.png" alt="">
|
||||||
|
<span class="txt-label">给水和排水</span><span class="txt-number">35</span>
|
||||||
|
</div>
|
||||||
|
<div class="txt-item txt-item-4">
|
||||||
|
<img src="/imgs/type4.png" alt="">
|
||||||
|
<span class="txt-label">站内客运设备</span><span class="txt-number">49</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="ellipse"></div>
|
||||||
|
<div class="ellipse ellipse-2"></div>
|
||||||
|
<div class="chart-header">
|
||||||
|
<div class="legend-container">
|
||||||
|
<span class="legend-icon"></span>
|
||||||
|
<span class="legend-text">维修次数</span>
|
||||||
|
</div>
|
||||||
|
<div class="year-text">2026 年</div>
|
||||||
|
</div>
|
||||||
|
<div class="chart-container">
|
||||||
|
<v-chart class="bar-chart" :option="chartOption" autoresize />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import VChart from 'vue-echarts'
|
||||||
|
import * as echarts from 'echarts'
|
||||||
|
|
||||||
|
const months = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12']
|
||||||
|
|
||||||
|
const maintenanceData = [145, 142, 138, 148, 143, 140, 146, 144, 139, 147, 141, 145]
|
||||||
|
|
||||||
|
const chartOption = computed(() => ({
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'axis',
|
||||||
|
axisPointer: {
|
||||||
|
type: 'shadow'
|
||||||
|
},
|
||||||
|
backgroundColor: 'rgba(10, 25, 50, 0.95)',
|
||||||
|
borderColor: 'rgba(0, 200, 255, 0.4)',
|
||||||
|
borderWidth: 1,
|
||||||
|
padding: [8, 12],
|
||||||
|
textStyle: {
|
||||||
|
color: '#fff',
|
||||||
|
fontSize: 13
|
||||||
|
},
|
||||||
|
formatter: '{b}月<br/>维修次数:{c} 次'
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
left: '10%',
|
||||||
|
right: '15%',
|
||||||
|
bottom: '15%',
|
||||||
|
top: '10%',
|
||||||
|
containLabel: true
|
||||||
|
},
|
||||||
|
xAxis: {
|
||||||
|
type: 'category',
|
||||||
|
data: months,
|
||||||
|
name: '月份',
|
||||||
|
nameLocation: 'end',
|
||||||
|
nameGap: 20,
|
||||||
|
nameTextStyle: {
|
||||||
|
color: 'rgba(150, 210, 255, 0.85)',
|
||||||
|
fontSize: 13,
|
||||||
|
fontFamily: 'Arial'
|
||||||
|
},
|
||||||
|
axisLine: {
|
||||||
|
show: true,
|
||||||
|
lineStyle: {
|
||||||
|
color: 'rgba(100, 180, 255, 0.3)',
|
||||||
|
width: 1,
|
||||||
|
type: 'solid'
|
||||||
|
},
|
||||||
|
symbol: ['none', 'arrow'],
|
||||||
|
symbolSize: [8, 6],
|
||||||
|
symbolOffset: [0, 5]
|
||||||
|
},
|
||||||
|
axisTick: {
|
||||||
|
show: false
|
||||||
|
},
|
||||||
|
axisLabel: {
|
||||||
|
color: 'rgba(150, 210, 255, 0.85)',
|
||||||
|
fontSize: 13,
|
||||||
|
fontFamily: 'Arial'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
type: 'value',
|
||||||
|
min: 0,
|
||||||
|
max: 160,
|
||||||
|
interval: 20,
|
||||||
|
name: '单位:次',
|
||||||
|
nameLocation: 'end',
|
||||||
|
nameGap: 5,
|
||||||
|
nameRotate: 0,
|
||||||
|
nameTextStyle: {
|
||||||
|
color: 'rgba(150, 210, 255, 0.85)',
|
||||||
|
fontSize: 12,
|
||||||
|
fontFamily: 'Arial',
|
||||||
|
align: 'left'
|
||||||
|
},
|
||||||
|
axisLine: {
|
||||||
|
show: true,
|
||||||
|
lineStyle: {
|
||||||
|
color: 'rgba(100, 180, 255, 0.3)',
|
||||||
|
width: 1,
|
||||||
|
type: 'solid'
|
||||||
|
},
|
||||||
|
symbol: ['none', 'arrow'],
|
||||||
|
symbolSize: [8, 6],
|
||||||
|
symbolOffset: [5, 0]
|
||||||
|
},
|
||||||
|
axisTick: {
|
||||||
|
show: false
|
||||||
|
},
|
||||||
|
axisLabel: {
|
||||||
|
color: 'rgba(150, 210, 255, 0.85)',
|
||||||
|
fontSize: 12,
|
||||||
|
fontFamily: 'Arial',
|
||||||
|
padding: [0, 0, 0, 5]
|
||||||
|
},
|
||||||
|
splitLine: {
|
||||||
|
show: true,
|
||||||
|
lineStyle: {
|
||||||
|
color: 'rgba(100, 180, 255, 0.15)',
|
||||||
|
width: 1,
|
||||||
|
type: 'solid'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
series: [{
|
||||||
|
name: '维修次数',
|
||||||
|
type: 'bar',
|
||||||
|
barWidth: '35%',
|
||||||
|
data: maintenanceData.map(value => ({
|
||||||
|
value,
|
||||||
|
itemStyle: {
|
||||||
|
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||||
|
{
|
||||||
|
offset: 0,
|
||||||
|
color: 'rgba(0, 73, 229, 0.8)'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
offset: 1,
|
||||||
|
color: 'rgba(0, 246, 236, 0.16)'
|
||||||
|
}
|
||||||
|
]),
|
||||||
|
borderRadius: [6, 6, 0, 0]
|
||||||
|
}
|
||||||
|
})),
|
||||||
|
emphasis: {
|
||||||
|
itemStyle: {
|
||||||
|
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||||
|
{
|
||||||
|
offset: 0,
|
||||||
|
color: 'rgba(0, 73, 229, 0.95)'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
offset: 1,
|
||||||
|
color: 'rgba(0, 246, 236, 0.3)'
|
||||||
|
}
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}))
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.module-seven {
|
||||||
|
width: 892px;
|
||||||
|
height: 549px;
|
||||||
|
position: absolute;
|
||||||
|
left: 2948px;
|
||||||
|
top: 33px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: url('/imgs/msevenbg1.png') no-repeat center center;
|
||||||
|
background-size: cover;
|
||||||
|
backdrop-filter: blur(7px);
|
||||||
|
|
||||||
|
.module-body {
|
||||||
|
flex: 1;
|
||||||
|
position: relative;
|
||||||
|
padding: 15px 20px 10px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.time-range {
|
||||||
|
position: absolute;
|
||||||
|
top: 8px;
|
||||||
|
left: 160px;
|
||||||
|
font-family: Alibaba PuHuiTi 3.0;
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: normal;
|
||||||
|
letter-spacing: 0px;
|
||||||
|
font-variation-settings: "opsz" auto;
|
||||||
|
color: #FFFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.txt {
|
||||||
|
position: absolute;
|
||||||
|
top: 0px;
|
||||||
|
left: 50%;
|
||||||
|
width: 50%;
|
||||||
|
height: 50%;
|
||||||
|
// background-color: yellow;
|
||||||
|
|
||||||
|
.txt-item {
|
||||||
|
position: absolute;
|
||||||
|
color: #FFFFFF;
|
||||||
|
font-family: Alibaba PuHuiTi 3.0;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 400;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
width: calc(100% - 140px);
|
||||||
|
left: 40px;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-right: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.txt-label {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.txt-number {
|
||||||
|
min-width: 60px;
|
||||||
|
text-align: right;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.txt-item-1 {
|
||||||
|
top: 65px;
|
||||||
|
left: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.txt-item-2 {
|
||||||
|
top: 120px;
|
||||||
|
left: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.txt-item-3 {
|
||||||
|
top: 176px;
|
||||||
|
left: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.txt-item-4 {
|
||||||
|
top: 236px;
|
||||||
|
left: 40px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-header {
|
||||||
|
height: 36px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
position: absolute;
|
||||||
|
top: 290px;
|
||||||
|
left: 50%;
|
||||||
|
right: 20px;
|
||||||
|
|
||||||
|
.legend-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
.legend-icon {
|
||||||
|
width: 16px;
|
||||||
|
height: 10px;
|
||||||
|
background: linear-gradient(to bottom, #00E5FF 0%, #00BFFF 50%, #0066CC 100%);
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-text {
|
||||||
|
color: #fff;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.year-text {
|
||||||
|
color: rgba(150, 210, 255, 0.9);
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 400;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ellipse {
|
||||||
|
position: absolute;
|
||||||
|
left: 100px;
|
||||||
|
top: 160px;
|
||||||
|
width: 193.99px;
|
||||||
|
height: 78.66px;
|
||||||
|
opacity: 0.6;
|
||||||
|
background: rgba(196, 196, 196, 0.0001);
|
||||||
|
box-sizing: border-box;
|
||||||
|
border: 1px solid rgba(71, 255, 255, 0.6);
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: float 3s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ellipse-2 {
|
||||||
|
left: 40px;
|
||||||
|
top: 100px;
|
||||||
|
width: 311px;
|
||||||
|
height: 147.31px;
|
||||||
|
opacity: 1;
|
||||||
|
border: 1px solid rgba(71, 255, 255, 0.4);
|
||||||
|
box-shadow: inset 0px 0px 10px 0px #47FFFF;
|
||||||
|
animation: float 3s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes float {
|
||||||
|
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
transform: translateY(5px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 280px;
|
||||||
|
position: absolute;
|
||||||
|
top: 305px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
|
||||||
|
.bar-chart {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,456 @@
|
||||||
|
<template>
|
||||||
|
<div class="module-six">
|
||||||
|
<div class="module-body">
|
||||||
|
<div class="charts-container">
|
||||||
|
<div class="compare-date">({{ charts[0]?.compareDate }})</div>
|
||||||
|
<div v-for="(chart, index) in charts" :key="index" class="chart-wrapper">
|
||||||
|
<div class="chart-header">
|
||||||
|
<div class="chart-number">
|
||||||
|
<span class="number-box"
|
||||||
|
:style="{ backgroundColor: getNumberBoxColor(chart.number), color: getNumberBoxTextColor(chart.number) }">{{
|
||||||
|
chart.number }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="chart-title">
|
||||||
|
<div class="title-text">累计</div>
|
||||||
|
<div class="title-text">客流</div>
|
||||||
|
</div>
|
||||||
|
<div class="baifengbi">
|
||||||
|
<span :style="getBaifenbiColorStyle(chart)">{{ getBaifenbiPercentage(chart) }}%</span>
|
||||||
|
</div>
|
||||||
|
<div class="chart-digits">
|
||||||
|
<div v-for="(digit, digitIndex) in chart.digits" :key="digitIndex" class="digit-box">
|
||||||
|
{{ digit }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="chart-content" :ref="el => chartRefs[index] = el"></div>
|
||||||
|
<div class="max-date">历史峰值:{{ chart.maxDate }} <span style="color: #ff3333; font-weight: bold; opacity: 1;">{{
|
||||||
|
Number(chart.maxValue).toLocaleString()
|
||||||
|
}}</span><span style="font-size: 14px;">人</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted, nextTick } from 'vue'
|
||||||
|
import * as echarts from 'echarts'
|
||||||
|
import server from '@/utils/service'
|
||||||
|
|
||||||
|
const chartRefs = ref([])
|
||||||
|
|
||||||
|
const xLabels = ['6', '8', '10', '12', '14', '16', '18', '20', '22', '24']
|
||||||
|
|
||||||
|
const chartDataList = ref([
|
||||||
|
{ number: 0, barData: [], yellowData: [], redData: [], digits: [], compareDate: '', maxDate: '', maxValue: 0, compareMaxValue: 0 },
|
||||||
|
{ number: 0, barData: [], yellowData: [], redData: [], digits: [], compareDate: '', maxDate: '', maxValue: 0, compareMaxValue: 0 },
|
||||||
|
{ number: 0, barData: [], yellowData: [], redData: [], digits: [], compareDate: '', maxDate: '', maxValue: 0, compareMaxValue: 0 },
|
||||||
|
{ number: 0, barData: [], yellowData: [], redData: [], digits: [], compareDate: '', maxDate: '', maxValue: 0, compareMaxValue: 0 }
|
||||||
|
])
|
||||||
|
|
||||||
|
const charts = ref(chartDataList.value)
|
||||||
|
|
||||||
|
const chartInstances = ref([])
|
||||||
|
|
||||||
|
const timeKeys = ['time1', 'time2', 'time3', 'time4', 'time5', 'time6', 'time7', 'time8', 'time9', 'time10']
|
||||||
|
|
||||||
|
const getNumberBoxColor = (number) => {
|
||||||
|
const colors = {
|
||||||
|
3: '#FBD61D',
|
||||||
|
4: '#451E83',
|
||||||
|
7: '#ED7010',
|
||||||
|
15: '#C4AF8C'
|
||||||
|
}
|
||||||
|
return colors[number] || '#ffcc00'
|
||||||
|
}
|
||||||
|
|
||||||
|
const getNumberBoxTextColor = (number) => {
|
||||||
|
const textColors = {
|
||||||
|
4: '#ffffff'
|
||||||
|
}
|
||||||
|
return textColors[number] || '#000000'
|
||||||
|
}
|
||||||
|
|
||||||
|
const getBaifenbiPercentage = (chart) => {
|
||||||
|
if (!chart.compareMaxValue || chart.compareMaxValue === 0) {
|
||||||
|
return '0.00'
|
||||||
|
}
|
||||||
|
console.log("今日客流累计 对比 对比日当前时间点客流:", chart.totalSum, chart.compareMaxValue)
|
||||||
|
|
||||||
|
const percentage = ((chart.totalSum - chart.compareMaxValue) / chart.compareMaxValue) * 100
|
||||||
|
// return Math.abs(percentage).toFixed(2)
|
||||||
|
return percentage.toFixed(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getBaifenbiColorStyle = (chart) => {
|
||||||
|
if (!chart.compareMaxValue || chart.compareMaxValue === 0) {
|
||||||
|
return { color: '#ffffff', fontWeight: 'bold' }
|
||||||
|
}
|
||||||
|
const percentage = (chart.totalSum - chart.compareMaxValue) / chart.compareMaxValue
|
||||||
|
if (percentage > 0) {
|
||||||
|
return { color: '#ff3333', fontWeight: 'bold' }
|
||||||
|
} else if (percentage < 0) {
|
||||||
|
return { color: '#00ff00', fontWeight: 'bold' }
|
||||||
|
} else {
|
||||||
|
return { color: '#ffffff', fontWeight: 'bold' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const processData = (dataList) => {
|
||||||
|
// 根据当前系统时间计算对应的 time 索引
|
||||||
|
const getCurrentTimeIndex = () => {
|
||||||
|
const now = new Date()
|
||||||
|
const hour = now.getHours()
|
||||||
|
const minutes = now.getMinutes()
|
||||||
|
// 5 点和 6 点对应 time1,7 点和 8 点对应 time2,以此类推
|
||||||
|
// time1: 5-6, time2: 7-8, time3: 9-10, time4: 11-12, time5: 13-14
|
||||||
|
// time6: 15-16, time7: 17-18, time8: 19-20, time9: 21-22, time10: 23-24
|
||||||
|
if (hour < 5) return 0
|
||||||
|
// 如果当前小时是偶数且分钟大于 0,或者当前小时是奇数,都算下一个时间段
|
||||||
|
// 例如:10:30,10 是偶数,但已经过了 10 点,应该算 time4
|
||||||
|
// 11:00-11:59 也算 time4,12:00-12:59 也算 time4
|
||||||
|
const timeIndex = Math.ceil((hour - 5) / 2)
|
||||||
|
const result = Math.min(timeIndex, 9) // 最大到 time10 (索引 9)
|
||||||
|
console.log(`当前时间:${hour}:${minutes},计算得到的 time 索引:${result}, 对应 ${timeKeys[result]}`)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentTimeIndex = getCurrentTimeIndex()
|
||||||
|
|
||||||
|
return dataList.map(item => {
|
||||||
|
const getValues = (dataObj) => {
|
||||||
|
return timeKeys.map(key => {
|
||||||
|
const value = dataObj[key]
|
||||||
|
return value ? parseFloat(value) : 0
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const barData = getValues(item.today)
|
||||||
|
const yellowData = getValues(item.compareDay)
|
||||||
|
const redData = getValues(item.maxDay)
|
||||||
|
|
||||||
|
// 根据当前时间获取对应的 maxDay 值和 compareDay 值
|
||||||
|
const maxValue = redData[currentTimeIndex]
|
||||||
|
const compareMaxValue = yellowData[currentTimeIndex]
|
||||||
|
const totalSum = item.today?.totalSum ? parseFloat(item.today.totalSum) : 0
|
||||||
|
|
||||||
|
const total = barData.reduce((sum, val) => sum + val, 0)
|
||||||
|
const totalStr = Math.floor(total).toString()
|
||||||
|
const digits = totalStr.split('').map(Number)
|
||||||
|
|
||||||
|
return {
|
||||||
|
barData,
|
||||||
|
yellowData,
|
||||||
|
redData,
|
||||||
|
number: parseInt(item.line),
|
||||||
|
digits: digits.length > 0 ? digits : [0],
|
||||||
|
compareDate: item.compareDate,
|
||||||
|
maxDate: item.maxDay?.maxDate || '',
|
||||||
|
maxValue: maxValue,
|
||||||
|
compareMaxValue: compareMaxValue,
|
||||||
|
totalSum: totalSum
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchYunyingInfo = async () => {
|
||||||
|
try {
|
||||||
|
const response = await server.getYunyingInfo()
|
||||||
|
if (response.data.success) {
|
||||||
|
console.log('历史峰值:', response.data.data)
|
||||||
|
const processedData = processData(response.data.data)
|
||||||
|
console.log('************************************************')
|
||||||
|
processedData.forEach((data, index) => {
|
||||||
|
if (index < chartDataList.value.length) {
|
||||||
|
chartDataList.value[index] = data
|
||||||
|
}
|
||||||
|
})
|
||||||
|
charts.value = [...chartDataList.value]
|
||||||
|
await nextTick()
|
||||||
|
initCharts()
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取运营信息失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const initCharts = () => {
|
||||||
|
chartRefs.value.forEach((el, index) => {
|
||||||
|
if (el && charts.value[index]) {
|
||||||
|
if (chartInstances.value[index]) {
|
||||||
|
chartInstances.value[index].dispose()
|
||||||
|
}
|
||||||
|
const chart = echarts.init(el)
|
||||||
|
const data = charts.value[index]
|
||||||
|
chart.setOption(getChartOption(data.barData, data.yellowData, data.redData))
|
||||||
|
chartInstances.value[index] = chart
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
window.addEventListener('resize', () => {
|
||||||
|
chartInstances.value.forEach(chart => {
|
||||||
|
if (chart) chart.resize()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const getChartOption = (barData, yellowData, redData) => {
|
||||||
|
const maxValue = Math.max(...barData, ...yellowData, ...redData)
|
||||||
|
const yAxisMax = Math.ceil(maxValue / 10000) * 10000
|
||||||
|
|
||||||
|
return {
|
||||||
|
grid: {
|
||||||
|
left: 30,
|
||||||
|
right: 10,
|
||||||
|
top: 10,
|
||||||
|
bottom: 30
|
||||||
|
},
|
||||||
|
xAxis: {
|
||||||
|
type: 'category',
|
||||||
|
data: xLabels,
|
||||||
|
axisLine: {
|
||||||
|
lineStyle: {
|
||||||
|
color: '#6ab'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
axisLabel: {
|
||||||
|
color: '#6ab',
|
||||||
|
fontSize: 12,
|
||||||
|
margin: 10
|
||||||
|
},
|
||||||
|
axisTick: {
|
||||||
|
show: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
type: 'value',
|
||||||
|
min: 0,
|
||||||
|
max: yAxisMax,
|
||||||
|
interval: yAxisMax / 5,
|
||||||
|
axisLine: {
|
||||||
|
show: false
|
||||||
|
},
|
||||||
|
axisLabel: {
|
||||||
|
color: '#6ab',
|
||||||
|
fontSize: 12,
|
||||||
|
formatter: (value) => {
|
||||||
|
return (value / 10000).toFixed(0)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
splitLine: {
|
||||||
|
lineStyle: {
|
||||||
|
type: 'dashed',
|
||||||
|
color: 'rgba(106, 170, 187, 0.3)',
|
||||||
|
width: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: '实际',
|
||||||
|
type: 'line',
|
||||||
|
data: barData,
|
||||||
|
symbol: 'circle',
|
||||||
|
showSymbol: true,
|
||||||
|
symbolSize: 6,
|
||||||
|
lineStyle: {
|
||||||
|
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||||
|
{ offset: 0, color: 'rgba(0, 212, 255, 1)' },
|
||||||
|
{ offset: 1, color: 'rgba(0, 212, 255, 1)' }
|
||||||
|
]),
|
||||||
|
width: 2
|
||||||
|
},
|
||||||
|
itemStyle: {
|
||||||
|
color: 'rgba(0, 212, 255, 1)',
|
||||||
|
borderColor: '#fff',
|
||||||
|
borderWidth: 1
|
||||||
|
},
|
||||||
|
areaStyle: {
|
||||||
|
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||||
|
{ offset: 0, color: 'rgba(0, 212, 255, 0.8)' },
|
||||||
|
{ offset: 1, color: 'rgba(0, 212, 255, 0.1)' }
|
||||||
|
])
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '对比日',
|
||||||
|
type: 'line',
|
||||||
|
data: yellowData,
|
||||||
|
smooth: true,
|
||||||
|
symbol: 'none',
|
||||||
|
itemStyle: {
|
||||||
|
color: '#ffcc00'
|
||||||
|
},
|
||||||
|
lineStyle: {
|
||||||
|
color: '#ffcc00',
|
||||||
|
width: 2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '历史峰值',
|
||||||
|
type: 'line',
|
||||||
|
data: redData,
|
||||||
|
smooth: true,
|
||||||
|
symbol: 'none',
|
||||||
|
itemStyle: {
|
||||||
|
color: '#ff4444'
|
||||||
|
},
|
||||||
|
lineStyle: {
|
||||||
|
color: '#ff4444',
|
||||||
|
width: 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
|
||||||
|
fetchYunyingInfo()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.module-six {
|
||||||
|
width: 1250px;
|
||||||
|
height: 895px;
|
||||||
|
position: absolute;
|
||||||
|
left: 1689px;
|
||||||
|
top: 33px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: url('/imgs/msixbg.png') no-repeat center center;
|
||||||
|
background-size: cover;
|
||||||
|
|
||||||
|
.module-body {
|
||||||
|
flex: 1;
|
||||||
|
padding-top: 105px;
|
||||||
|
padding-left: 25px;
|
||||||
|
|
||||||
|
.charts-container {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 584px);
|
||||||
|
grid-template-rows: repeat(2, 371px);
|
||||||
|
gap: 20px;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
.compare-date {
|
||||||
|
position: absolute;
|
||||||
|
top: 61.5px;
|
||||||
|
left: 234px;
|
||||||
|
z-index: 10;
|
||||||
|
|
||||||
|
|
||||||
|
font-family: Alibaba PuHuiTi 3.0;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: normal;
|
||||||
|
letter-spacing: 0px;
|
||||||
|
|
||||||
|
font-variation-settings: "opsz" auto;
|
||||||
|
color: #FFFFFF;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-wrapper {
|
||||||
|
width: 584px;
|
||||||
|
height: 371px;
|
||||||
|
background-image: url('/imgs/chatbg.png');
|
||||||
|
background-size: cover;
|
||||||
|
// background: #1B53A5;
|
||||||
|
// opacity: 0.25;
|
||||||
|
border: 1px solid rgba(0, 212, 255, 0.2);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 15px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.chart-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 15px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
|
||||||
|
.chart-number {
|
||||||
|
.number-box {
|
||||||
|
display: inline-block;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
line-height: 40px;
|
||||||
|
text-align: center;
|
||||||
|
color: #000;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: bold;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-title {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 5px;
|
||||||
|
|
||||||
|
.title-text {
|
||||||
|
color: #fff;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.baifengbi {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
// font-family: Alibaba PuHuiTi 3.0;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
line-height: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-digits {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-left: auto;
|
||||||
|
|
||||||
|
.digit-box {
|
||||||
|
width: 45px;
|
||||||
|
height: 50px;
|
||||||
|
line-height: 50px;
|
||||||
|
text-align: center;
|
||||||
|
background: linear-gradient(180deg, #1e5799 0%, #0c2d4a 100%);
|
||||||
|
border: 1px solid #00d4ff;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: bold;
|
||||||
|
box-shadow: 0 0 10px rgba(0, 212, 255, 0.3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.max-date {
|
||||||
|
// position: absolute;
|
||||||
|
top: 25px;
|
||||||
|
right: 10px;
|
||||||
|
z-index: 10;
|
||||||
|
// font-family: Alibaba PuHuiTi 3.0;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: bold;
|
||||||
|
line-height: normal;
|
||||||
|
letter-spacing: 0px;
|
||||||
|
font-variation-settings: "opsz" auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-content {
|
||||||
|
flex: 1;
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,140 @@
|
||||||
|
<template>
|
||||||
|
<div class="module-ten">
|
||||||
|
<!-- <div class="module-title">模块十</div> -->
|
||||||
|
<div class="module-body">
|
||||||
|
<div class="video-wrapper">
|
||||||
|
<video class="video-ats" controls autoplay muted playsinline loop></video>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { onMounted, nextTick, onBeforeUnmount } from 'vue'
|
||||||
|
import HLS from 'hls.js'
|
||||||
|
|
||||||
|
let hlsInstance = null
|
||||||
|
|
||||||
|
const initVideo = async () => {
|
||||||
|
await nextTick()
|
||||||
|
setTimeout(() => {
|
||||||
|
const video = document.querySelector('.video-ats')
|
||||||
|
if (!video) return
|
||||||
|
|
||||||
|
if (hlsInstance) {
|
||||||
|
hlsInstance.destroy()
|
||||||
|
hlsInstance = null
|
||||||
|
}
|
||||||
|
|
||||||
|
const videoUrl = `http://10.107.7.19:8100/main.m3u8`
|
||||||
|
|
||||||
|
if (HLS.isSupported()) {
|
||||||
|
const hls = new HLS({
|
||||||
|
debug: true,
|
||||||
|
liveSyncDuration: 3,
|
||||||
|
maxBufferLength: 30
|
||||||
|
})
|
||||||
|
hlsInstance = hls
|
||||||
|
|
||||||
|
hls.on(HLS.Events.MANIFEST_PARSED, () => {
|
||||||
|
console.log('视频 manifest 解析完成')
|
||||||
|
})
|
||||||
|
|
||||||
|
hls.on(HLS.Events.LEVEL_LOADED, () => {
|
||||||
|
console.log('视频级别加载完成')
|
||||||
|
})
|
||||||
|
|
||||||
|
hls.on(HLS.Events.FRAME_LOADED, () => {
|
||||||
|
console.log('视频帧加载完成')
|
||||||
|
})
|
||||||
|
|
||||||
|
hls.on(HLS.Events.ERROR, (event, data) => {
|
||||||
|
console.error('视频 HLS 错误:', event, data)
|
||||||
|
if (data.fatal) {
|
||||||
|
hls.destroy()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
hls.on(HLS.Events.MEDIA_PLAYING, () => {
|
||||||
|
console.log('视频开始播放')
|
||||||
|
})
|
||||||
|
|
||||||
|
hls.loadSource(videoUrl)
|
||||||
|
hls.attachMedia(video)
|
||||||
|
|
||||||
|
video.play().then(() => {
|
||||||
|
console.log('视频开始播放')
|
||||||
|
}).catch(err => {
|
||||||
|
console.error('视频播放失败:', err)
|
||||||
|
})
|
||||||
|
} else if (video.canPlayType('application/x-mpegURL')) {
|
||||||
|
video.src = videoUrl
|
||||||
|
video.load()
|
||||||
|
video.play().then(() => {
|
||||||
|
console.log('视频开始播放')
|
||||||
|
}).catch(err => {
|
||||||
|
console.error('视频播放失败:', err)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
console.error('浏览器不支持 HLS')
|
||||||
|
}
|
||||||
|
}, 500)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
initVideo()
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
if (hlsInstance) {
|
||||||
|
hlsInstance.destroy()
|
||||||
|
hlsInstance = null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.module-ten {
|
||||||
|
width: 2923px;
|
||||||
|
height: 535px;
|
||||||
|
position: absolute;
|
||||||
|
left: 17px;
|
||||||
|
top: 957px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: url('/imgs/mtenbg.png') no-repeat center center;
|
||||||
|
background-size: cover;
|
||||||
|
|
||||||
|
.module-title {
|
||||||
|
height: 40px;
|
||||||
|
line-height: 40px;
|
||||||
|
padding: 0 16px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #00d4ff;
|
||||||
|
background: rgba(0, 212, 255, 0.1);
|
||||||
|
border-bottom: 1px solid rgba(0, 212, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-body {
|
||||||
|
flex: 1;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.video-wrapper {
|
||||||
|
position: absolute;
|
||||||
|
left: 35px;
|
||||||
|
top: 35px;
|
||||||
|
width: 2851px;
|
||||||
|
height: 455px;
|
||||||
|
opacity: 1;
|
||||||
|
|
||||||
|
.video-ats {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: fill;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,182 @@
|
||||||
|
<template>
|
||||||
|
<div class="module-three">
|
||||||
|
<!-- <div class="module-title">模块三</div> -->
|
||||||
|
<div class="module-body" v-if="videoList.length">
|
||||||
|
<div class="video-row video-row-top">
|
||||||
|
<div class="video-wrapper" v-for="(item, index) in videoList.slice(0, 2)" :key="'top-' + index">
|
||||||
|
<video class="video-item" controls autoplay muted playsinline loop></video>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="video-row video-row-bottom">
|
||||||
|
<div class="video-wrapper" v-for="(item, index) in videoList.slice(2, 4)" :key="'bottom-' + index">
|
||||||
|
<video class="video-item" controls autoplay muted playsinline loop></video>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted, nextTick } from 'vue'
|
||||||
|
import HLS from 'hls.js'
|
||||||
|
import server from '@/utils/service'
|
||||||
|
|
||||||
|
const videoList = ref([])
|
||||||
|
|
||||||
|
const fetchVideoList = async () => {
|
||||||
|
try {
|
||||||
|
const response = await server.getCommonMonitor()
|
||||||
|
if (response.data.success) {
|
||||||
|
videoList.value = response.data.data.map(item => ({
|
||||||
|
url: item.url.trim(),
|
||||||
|
line: item.line,
|
||||||
|
station: item.station,
|
||||||
|
monitor: item.monitor
|
||||||
|
}))
|
||||||
|
console.log('视频列表数据:', videoList.value)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取视频列表失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const initVideos = async () => {
|
||||||
|
console.log('开始初始化视频...')
|
||||||
|
await fetchVideoList()
|
||||||
|
console.log('获取到视频列表:', videoList.value.length)
|
||||||
|
|
||||||
|
if (!videoList.value.length) {
|
||||||
|
console.warn('视频列表为空,跳过视频初始化')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await nextTick()
|
||||||
|
console.log('DOM 更新完成,开始播放视频')
|
||||||
|
setTimeout(() => {
|
||||||
|
const videos = document.querySelectorAll('.video-item')
|
||||||
|
console.log('找到视频元素数量:', videos.length)
|
||||||
|
videos.forEach((video, index) => {
|
||||||
|
console.log('初始化视频', index + 1, '...')
|
||||||
|
|
||||||
|
const videoItem = videoList.value[index % videoList.value.length]
|
||||||
|
if (!videoItem || !videoItem.url) {
|
||||||
|
console.warn('视频', index + 1, '无有效URL,跳过')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const videoUrl = videoItem.url
|
||||||
|
console.log('视频', index + 1, 'URL:', videoUrl)
|
||||||
|
|
||||||
|
if (HLS.isSupported()) {
|
||||||
|
console.log('浏览器支持 HLS,使用 HLS.js')
|
||||||
|
const hls = new HLS({
|
||||||
|
debug: true,
|
||||||
|
liveSyncDuration: 3,
|
||||||
|
maxBufferLength: 30
|
||||||
|
})
|
||||||
|
|
||||||
|
hls.on(HLS.Events.MANIFEST_PARSED, () => {
|
||||||
|
console.log('视频', index + 1, 'manifest 解析完成')
|
||||||
|
})
|
||||||
|
|
||||||
|
hls.on(HLS.Events.LEVEL_LOADED, () => {
|
||||||
|
console.log('视频', index + 1, '级别加载完成')
|
||||||
|
})
|
||||||
|
|
||||||
|
hls.on(HLS.Events.FRAME_LOADED, () => {
|
||||||
|
console.log('视频', index + 1, '帧加载完成')
|
||||||
|
})
|
||||||
|
|
||||||
|
hls.on(HLS.Events.ERROR, (event, data) => {
|
||||||
|
console.error('视频', index + 1, 'HLS 错误:', event, data)
|
||||||
|
if (data.fatal) {
|
||||||
|
hls.destroy()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
hls.on(HLS.Events.MEDIA_PLAYING, () => {
|
||||||
|
console.log('视频', index + 1, 'HLS 开始播放')
|
||||||
|
})
|
||||||
|
|
||||||
|
hls.loadSource(videoUrl)
|
||||||
|
hls.attachMedia(video)
|
||||||
|
|
||||||
|
video.play().then(() => {
|
||||||
|
console.log('视频', index + 1, '开始播放')
|
||||||
|
}).catch(err => {
|
||||||
|
console.error('视频', index + 1, '播放失败:', err)
|
||||||
|
})
|
||||||
|
} else if (video.canPlayType('application/x-mpegURL')) {
|
||||||
|
console.log('浏览器原生支持 HLS')
|
||||||
|
video.src = videoUrl
|
||||||
|
video.load()
|
||||||
|
video.play().then(() => {
|
||||||
|
console.log('视频', index + 1, '开始播放')
|
||||||
|
}).catch(err => {
|
||||||
|
console.error('视频', index + 1, '播放失败:', err)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
console.error('浏览器不支持 HLS')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, 500)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
console.log('ModuleThree 组件已挂载')
|
||||||
|
initVideos()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.module-three {
|
||||||
|
width: 866px;
|
||||||
|
height: 583px;
|
||||||
|
position: absolute;
|
||||||
|
left: 17px;
|
||||||
|
top: 344px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: url('/imgs/mthreebg.png') no-repeat center center;
|
||||||
|
background-size: cover;
|
||||||
|
|
||||||
|
.module-title {
|
||||||
|
height: 40px;
|
||||||
|
line-height: 40px;
|
||||||
|
padding: 0 16px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #00d4ff;
|
||||||
|
background: rgba(0, 212, 255, 0.1);
|
||||||
|
border-bottom: 1px solid rgba(0, 212, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-body {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20px;
|
||||||
|
|
||||||
|
.video-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
|
||||||
|
.video-wrapper {
|
||||||
|
width: 400px;
|
||||||
|
height: 246px;
|
||||||
|
overflow: hidden;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
.video-item {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: fill;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,184 @@
|
||||||
|
<template>
|
||||||
|
<div class="module-two">
|
||||||
|
<div class="module-body">
|
||||||
|
<div class="scroll-wrapper">
|
||||||
|
<div class="scroll-list"
|
||||||
|
:style="{ transform: `translateY(-${currentIndex * itemHeight}px)`, transition: transitionState ? 'transform 0.5s ease-in-out' : 'none' }">
|
||||||
|
<div class="person-row" v-for="(index) in Math.ceil(displayList.length / 2)" :key="index">
|
||||||
|
<div class="person-item" :class="{ 'full-width': !displayList[index * 2 - 1] }"
|
||||||
|
v-if="displayList[index * 2 - 2]">
|
||||||
|
<img :src="displayList[index * 2 - 2].sex === '1' ? '/imgs/man.png' : '/imgs/girl.png'" alt=""
|
||||||
|
class="person-icon" />
|
||||||
|
<span class="person-name">{{ displayList[index * 2 - 2].name }}</span>
|
||||||
|
<!-- <span class="person-phone">{{ displayList[index * 2 - 2].phone }}</span> -->
|
||||||
|
</div>
|
||||||
|
<div class="person-item" v-if="displayList[index * 2 - 1]">
|
||||||
|
<img :src="displayList[index * 2 - 1].sex === '1' ? '/imgs/man.png' : '/imgs/girl.png'" alt=""
|
||||||
|
class="person-icon" />
|
||||||
|
<span class="person-name">{{ displayList[index * 2 - 1].name }}</span>
|
||||||
|
<!-- <span class="person-phone">{{ displayList[index * 2 - 1].phone }}</span> -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||||
|
import server from '@/utils/service'
|
||||||
|
|
||||||
|
const personData = ref([])
|
||||||
|
|
||||||
|
const fetchDutyList = async () => {
|
||||||
|
try {
|
||||||
|
const response = await server.getDutyList()
|
||||||
|
if (response.data.success) {
|
||||||
|
console.log('原始数据:', response.data.data)
|
||||||
|
personData.value = response.data.data.map(item => ({
|
||||||
|
name: item.dutuser,
|
||||||
|
phone: item.phone,
|
||||||
|
sex: item.sex
|
||||||
|
}))
|
||||||
|
console.log('值班人员列表:', personData.value)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取值班列表失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentIndex = ref(0)
|
||||||
|
const transitionState = ref(true)
|
||||||
|
let timer = null
|
||||||
|
const itemHeight = 44
|
||||||
|
const visibleCount = 5
|
||||||
|
|
||||||
|
const displayList = computed(() => {
|
||||||
|
if (personData.value.length <= visibleCount) {
|
||||||
|
return personData.value
|
||||||
|
}
|
||||||
|
return personData.value
|
||||||
|
})
|
||||||
|
|
||||||
|
const startScroll = () => {
|
||||||
|
if (personData.value.length <= visibleCount) return
|
||||||
|
|
||||||
|
// timer = setInterval(() => {
|
||||||
|
// currentIndex.value++
|
||||||
|
// if (currentIndex.value >= personData.value.length) {
|
||||||
|
// transitionState.value = false
|
||||||
|
// currentIndex.value = 0
|
||||||
|
// setTimeout(() => {
|
||||||
|
// transitionState.value = true
|
||||||
|
// }, 50)
|
||||||
|
// }
|
||||||
|
// }, 3000)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchDutyList().then(() => {
|
||||||
|
if (personData.value.length > visibleCount) {
|
||||||
|
startScroll()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (timer) {
|
||||||
|
clearInterval(timer)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.module-two {
|
||||||
|
width: 337px;
|
||||||
|
height: 281px;
|
||||||
|
position: absolute;
|
||||||
|
left: 546px;
|
||||||
|
top: 33px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: url('/imgs/mtwobg.png') no-repeat center center;
|
||||||
|
background-size: cover;
|
||||||
|
|
||||||
|
.module-body {
|
||||||
|
flex: 1;
|
||||||
|
padding: 40px 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.scroll-wrapper {
|
||||||
|
height: 220px;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-top: 20px;
|
||||||
|
|
||||||
|
.scroll-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
transition: transform 0.5s ease-in-out;
|
||||||
|
|
||||||
|
.person-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
background: transparent;
|
||||||
|
margin-top: 20px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
|
||||||
|
.person-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
width: 50%;
|
||||||
|
position: relative;
|
||||||
|
background: transparent;
|
||||||
|
// background: yellow;
|
||||||
|
|
||||||
|
&.full-width {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.person-icon {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
margin-right: 12px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.person-name {
|
||||||
|
flex: 1;
|
||||||
|
opacity: 1;
|
||||||
|
font-family: Alibaba PuHuiTi 3.0;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 18px;
|
||||||
|
letter-spacing: 0.26px;
|
||||||
|
//如果文字过长,换行文字间高度距离3px
|
||||||
|
word-break: break-word;
|
||||||
|
line-height: 26px;
|
||||||
|
|
||||||
|
font-variation-settings: "opsz" auto;
|
||||||
|
/* 填充/白/100% */
|
||||||
|
color: #FFFFFF;
|
||||||
|
text-align: left;
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.person-phone {
|
||||||
|
color: rgba(150, 210, 255, 0.9);
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
font-family: 'Alibaba PuHuiTi 3.0', sans-serif;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
<template>
|
||||||
|
<!-- 主体内容区 -->
|
||||||
|
<div class="main-content">
|
||||||
|
<!-- 模块1 -->
|
||||||
|
<ModuleOne />
|
||||||
|
<!-- 模块2 -->
|
||||||
|
<ModuleTwo />
|
||||||
|
<!-- 模块3 -->
|
||||||
|
<ModuleThree />
|
||||||
|
<!-- 模块4 -->
|
||||||
|
<ModuleFour />
|
||||||
|
<!-- 模块5 -->
|
||||||
|
<!-- <ModuleFive /> -->
|
||||||
|
<!-- 模块6 -->
|
||||||
|
<ModuleSix />
|
||||||
|
<!-- 模块7 -->
|
||||||
|
<ModuleSeven />
|
||||||
|
<!-- 模块8 -->
|
||||||
|
<ModuleEight />
|
||||||
|
<!-- 模块9 -->
|
||||||
|
|
||||||
|
<ModuleFive />
|
||||||
|
|
||||||
|
<ModuleNine />
|
||||||
|
<!-- 模块10 -->
|
||||||
|
<ModuleTen />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { onMounted, onUnmounted } from 'vue'
|
||||||
|
import ModuleOne from './components/ModuleOne.vue'
|
||||||
|
import ModuleTwo from './components/ModuleTwo.vue'
|
||||||
|
import ModuleThree from './components/ModuleThree.vue'
|
||||||
|
import ModuleFour from './components/ModuleFour.vue'
|
||||||
|
import ModuleFive from './components/ModuleFive.vue'
|
||||||
|
import ModuleSix from './components/ModuleSix.vue'
|
||||||
|
import ModuleSeven from './components/ModuleSeven.vue'
|
||||||
|
import ModuleEight from './components/ModuleEight.vue'
|
||||||
|
import ModuleNine from './components/ModuleNine.vue'
|
||||||
|
import ModuleTen from './components/ModuleTen.vue'
|
||||||
|
|
||||||
|
let refreshTimer = null
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
refreshTimer = setInterval(() => {
|
||||||
|
window.location.reload()
|
||||||
|
}, 60 * 60 * 1000)
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (refreshTimer) {
|
||||||
|
clearInterval(refreshTimer)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.main-content {
|
||||||
|
width: 4800px;
|
||||||
|
height: 1514px; // 1620 - 106
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
<template>
|
||||||
|
<div class="layout-container">
|
||||||
|
<DashboardHeader />
|
||||||
|
<div class="route-content">
|
||||||
|
<router-view />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import DashboardHeader from '@/views/home/components/DashboardHeader.vue'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.layout-container {
|
||||||
|
width: 4800px;
|
||||||
|
height: 1620px;
|
||||||
|
position: relative;
|
||||||
|
background: url('/imgs/mainBg.png') no-repeat center center;
|
||||||
|
background-size: cover;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.route-content {
|
||||||
|
width: 4800px;
|
||||||
|
height: 1514px;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,168 @@
|
||||||
|
<template>
|
||||||
|
<div class="login-container">
|
||||||
|
<div class="login-box">
|
||||||
|
<div class="login-header">
|
||||||
|
<h2>上海地铁运三运营有限公司</h2>
|
||||||
|
<p>数字监管平台</p>
|
||||||
|
</div>
|
||||||
|
<el-form ref="loginFormRef" :model="loginForm" :rules="rules" class="login-form">
|
||||||
|
<el-form-item prop="username">
|
||||||
|
<el-input
|
||||||
|
v-model="loginForm.username"
|
||||||
|
placeholder="请输入用户名"
|
||||||
|
:prefix-icon="User"
|
||||||
|
size="large"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item prop="password">
|
||||||
|
<el-input
|
||||||
|
v-model="loginForm.password"
|
||||||
|
type="password"
|
||||||
|
placeholder="请输入密码"
|
||||||
|
:prefix-icon="Lock"
|
||||||
|
size="large"
|
||||||
|
show-password
|
||||||
|
@keyup.enter="handleLogin"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
size="large"
|
||||||
|
:loading="loading"
|
||||||
|
class="login-button"
|
||||||
|
@click="handleLogin"
|
||||||
|
>
|
||||||
|
登 录
|
||||||
|
</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive } from 'vue'
|
||||||
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { User, Lock } from '@element-plus/icons-vue'
|
||||||
|
import { encrypt } from '@/utils/sm4'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const route = useRoute()
|
||||||
|
const loginFormRef = ref(null)
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
const loginForm = reactive({
|
||||||
|
username: 'admin',
|
||||||
|
password: 'admin123'
|
||||||
|
})
|
||||||
|
|
||||||
|
const rules = {
|
||||||
|
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
|
||||||
|
password: [{ required: true, message: '请输入密码', trigger: 'blur' }]
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleLogin = async () => {
|
||||||
|
if (!loginFormRef.value) return
|
||||||
|
|
||||||
|
await loginFormRef.value.validate(async (valid) => {
|
||||||
|
if (valid) {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const encryptedPassword = encrypt(loginForm.password)
|
||||||
|
localStorage.setItem('token', 'mock-token-' + Date.now())
|
||||||
|
localStorage.setItem('userInfo', JSON.stringify({
|
||||||
|
username: loginForm.username,
|
||||||
|
password: encryptedPassword
|
||||||
|
}))
|
||||||
|
|
||||||
|
ElMessage.success('登录成功')
|
||||||
|
const redirect = route.query.redirect || '/home'
|
||||||
|
router.push(redirect)
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('登录失败')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.login-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: linear-gradient(135deg, #0a1628 0%, #1a365d 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-box {
|
||||||
|
width: 460px;
|
||||||
|
padding: 48px;
|
||||||
|
background: rgba(10, 22, 40, 0.9);
|
||||||
|
border-radius: 16px;
|
||||||
|
border: 1px solid rgba(0, 212, 255, 0.3);
|
||||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
|
||||||
|
scale: 2.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #00d4ff;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #7eb8d8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-form {
|
||||||
|
:deep(.el-form-item) {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-input__wrapper) {
|
||||||
|
background: rgba(0, 212, 255, 0.05);
|
||||||
|
border: 1px solid rgba(0, 212, 255, 0.3);
|
||||||
|
box-shadow: none;
|
||||||
|
|
||||||
|
&:hover, &:focus {
|
||||||
|
border-color: #00d4ff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-input__inner) {
|
||||||
|
color: #e8f4ff;
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: #7eb8d8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-button {
|
||||||
|
width: 100%;
|
||||||
|
height: 48px;
|
||||||
|
font-size: 16px;
|
||||||
|
background: linear-gradient(135deg, #00d4ff 0%, #0099cc 100%);
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
color: #0a1628;
|
||||||
|
font-weight: 600;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: linear-gradient(135deg, #33ddff 0%, #00b8e6 100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,123 @@
|
||||||
|
<template>
|
||||||
|
<div class="traffic-container">
|
||||||
|
<section class="traffic-panel panel-rank-left"></section>
|
||||||
|
<section class="traffic-panel panel-rank-middle"></section>
|
||||||
|
<section class="traffic-panel panel-trend"></section>
|
||||||
|
<section class="traffic-panel panel-flow"></section>
|
||||||
|
<section class="traffic-panel panel-resource"></section>
|
||||||
|
<section class="traffic-panel panel-video"></section>
|
||||||
|
<section class="traffic-panel panel-list"></section>
|
||||||
|
<section class="traffic-panel panel-top"></section>
|
||||||
|
<section class="traffic-panel panel-events"></section>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.traffic-container {
|
||||||
|
width: 4800px;
|
||||||
|
height: 1514px;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.traffic-panel {
|
||||||
|
position: absolute;
|
||||||
|
border: 2px solid rgba(0, 186, 255, 0.72);
|
||||||
|
background:
|
||||||
|
linear-gradient(135deg, rgba(0, 90, 180, 0.18), rgba(0, 14, 56, 0.48)),
|
||||||
|
rgba(2, 22, 70, 0.42);
|
||||||
|
box-shadow:
|
||||||
|
inset 0 0 28px rgba(0, 188, 255, 0.16),
|
||||||
|
0 0 18px rgba(0, 150, 255, 0.2);
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&::before,
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
width: 42px;
|
||||||
|
height: 42px;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
left: -2px;
|
||||||
|
top: -2px;
|
||||||
|
border-left: 5px solid #00e5ff;
|
||||||
|
border-top: 5px solid #00e5ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
right: -2px;
|
||||||
|
bottom: -2px;
|
||||||
|
border-right: 5px solid #00e5ff;
|
||||||
|
border-bottom: 5px solid #00e5ff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-rank-left {
|
||||||
|
left: 16px;
|
||||||
|
top: 28px;
|
||||||
|
width: 880px;
|
||||||
|
height: 820px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-rank-middle {
|
||||||
|
left: 916px;
|
||||||
|
top: 28px;
|
||||||
|
width: 780px;
|
||||||
|
height: 820px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-trend {
|
||||||
|
left: 1716px;
|
||||||
|
top: 28px;
|
||||||
|
width: 1260px;
|
||||||
|
height: 820px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-flow {
|
||||||
|
left: 2996px;
|
||||||
|
top: 28px;
|
||||||
|
width: 886px;
|
||||||
|
height: 820px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-resource {
|
||||||
|
left: 3902px;
|
||||||
|
top: 28px;
|
||||||
|
width: 882px;
|
||||||
|
height: 820px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-video {
|
||||||
|
left: 16px;
|
||||||
|
top: 878px;
|
||||||
|
width: 2948px;
|
||||||
|
height: 608px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-list {
|
||||||
|
left: 2984px;
|
||||||
|
top: 878px;
|
||||||
|
width: 888px;
|
||||||
|
height: 608px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-top {
|
||||||
|
left: 3892px;
|
||||||
|
top: 878px;
|
||||||
|
width: 436px;
|
||||||
|
height: 608px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-events {
|
||||||
|
left: 4348px;
|
||||||
|
top: 878px;
|
||||||
|
width: 436px;
|
||||||
|
height: 608px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
import { resolve } from 'path'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [vue()],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': resolve(__dirname, 'src')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
host: '0.0.0.0',
|
||||||
|
port: 3000,
|
||||||
|
open: true,
|
||||||
|
proxy: {
|
||||||
|
'/juntech-ysdp': {
|
||||||
|
target: 'http://192.168.16.193:8888',
|
||||||
|
changeOrigin: true,
|
||||||
|
rewrite: (path) => path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
outDir: 'dist',
|
||||||
|
assetsDir: 'static',
|
||||||
|
sourcemap: false,
|
||||||
|
minify: 'terser',
|
||||||
|
terserOptions: {
|
||||||
|
compress: {
|
||||||
|
drop_console: true,
|
||||||
|
drop_debugger: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||