main
崔方鑫 2026-06-17 15:08:47 +08:00
commit 748deff833
74 changed files with 8682 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
node_modules
dist

2417
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

31
package.json Normal file
View File

@ -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"
}
}

41
public/README.md Normal file
View File

@ -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. 大型图片、字体文件建议放这里

18
public/data/config.json Normal file
View File

@ -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 }
}
}

Binary file not shown.

Binary file not shown.

BIN
public/imgs/Group630.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

BIN
public/imgs/cell.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

BIN
public/imgs/chatbg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

BIN
public/imgs/down.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 585 B

BIN
public/imgs/girl.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
public/imgs/guangyun.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
public/imgs/hot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
public/imgs/itembg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
public/imgs/liziqiu.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 641 KiB

BIN
public/imgs/mainBg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 MiB

BIN
public/imgs/man.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
public/imgs/meightbg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

BIN
public/imgs/mfivebg1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 683 KiB

BIN
public/imgs/mfourbg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

BIN
public/imgs/mninebg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

BIN
public/imgs/monebg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

BIN
public/imgs/monebg@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

BIN
public/imgs/msevenbg1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

BIN
public/imgs/msixbg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

BIN
public/imgs/mtenbg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

BIN
public/imgs/mthreebg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

BIN
public/imgs/mtwobg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

BIN
public/imgs/persion.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
public/imgs/title.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

BIN
public/imgs/tonebg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

BIN
public/imgs/tonebg1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
public/imgs/top.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 197 KiB

BIN
public/imgs/type1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
public/imgs/type2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

BIN
public/imgs/type3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 961 B

BIN
public/imgs/type4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 856 B

BIN
public/imgs/up.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 504 B

9
src/App.vue Normal file
View File

@ -0,0 +1,9 @@
<template>
<router-view />
</template>
<script setup>
</script>
<style lang="scss">
</style>

52
src/assets/README.md Normal file
View File

@ -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 |
|------|--------|--------|
| 构建处理 | ✅ 会被处理 | ❌ 原样复制 |
| 路径引用 | 相对路径或别名 | 根路径 |
| 适合场景 | 组件级资源 | 全局静态资源 |
| 文件大小优化 | ✅ 会被压缩 | ❌ 保持原样 |

Binary file not shown.

View File

@ -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;

View File

@ -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>

23
src/main.js Normal file
View File

@ -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()

74
src/router/index.js Normal file
View File

@ -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

103
src/styles/global.scss Normal file
View File

@ -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;
}

65
src/utils/assets.js Normal file
View File

@ -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'
}

3
src/utils/decrypt.js Normal file
View File

@ -0,0 +1,3 @@
import { encrypt, decrypt } from './sm4'
export { encrypt, decrypt }

50
src/utils/format.js Normal file
View File

@ -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)
}
}
}

86
src/utils/request.js Normal file
View File

@ -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 })
}
}

33
src/utils/scale.js Normal file
View File

@ -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)
}
}

177
src/utils/service.js Normal file
View File

@ -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

48
src/utils/sm4.js Normal file
View File

@ -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 }

View File

@ -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>

73
src/views/error/404.vue Normal file
View File

@ -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>

73
src/views/error/500.vue Normal file
View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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 time17 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:3010 10 time4
// 11:00-11:59 time412: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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

64
src/views/home/index.vue Normal file
View File

@ -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>

View File

@ -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>

168
src/views/login/index.vue Normal file
View File

@ -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>

123
src/views/traffic/index.vue Normal file
View File

@ -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>

36
vite.config.js Normal file
View File

@ -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
}
}
}
})

1149
yarn.lock Normal file

File diff suppressed because it is too large Load Diff