崔方鑫 2026-06-18 10:36:45 +08:00
parent 748deff833
commit d311f4631f
26 changed files with 1408 additions and 108 deletions

3
.env.development Normal file
View File

@ -0,0 +1,3 @@
# 开发环境配置
# 使用相对路径,通过 vite proxy 代理到目标服务器
VITE_API_BASE_URL=

2
.env.production Normal file
View File

@ -0,0 +1,2 @@
# 生产环境配置
VITE_API_BASE_URL=http://10.104.10.151:8888

170
README.md Normal file
View File

@ -0,0 +1,170 @@
# 云智控大屏系统
基于 Vue3 + Element Plus 的数据可视化大屏系统,支持 4800x1600 大屏自适应缩放。
## 功能特性
- **Vue3 Composition API** - 使用最新的 Vue3 语法
- **Element Plus** - 企业级 UI 组件库
- **模块化路由** - 清晰的路由结构,支持路由守卫
- **Axios 封装** - 统一的请求拦截、错误处理
- **SM4 加解密** - 国密算法支持(需要后端配合)
- **自适应缩放** - 根据浏览器窗口自动缩放到指定尺寸
- **大屏样式** - 深色主题,科技感设计
## 页面结构
```
src/
├── views/
│ ├── login/ # 登录页
│ ├── layout/ # 布局组件
│ ├── home/ # 首页
│ ├── dashboard/ # 数据大屏
│ └── error/ # 错误页面404/500
├── utils/
│ ├── request.js # Axios 请求封装
│ ├── sm4.js # SM4 加解密
│ ├── scale.js # 大屏缩放工具
│ └── format.js # 格式化工具
├── router/
│ └── index.js # 路由配置
└── styles/
└── global.scss # 全局样式
```
## 快速开始
### 安装依赖
```bash
npm install
```
### 启动开发服务器
```bash
npm run dev
```
### 构建生产版本
```bash
npm run build
```
## 默认登录账号
- 用户名admin
- 密码admin123
## 大屏配置
页面默认尺寸为 **4800 x 1600**,系统会自动根据浏览器窗口缩放。
如需修改尺寸,修改以下文件:
- `src/utils/scale.js` - 修改 `baseWidth``baseHeight`
## 接口配置
修改 `src/utils/request.js` 中的 `baseURL`,或创建 `.env` 文件:
```env
VITE_API_BASE_URL=http://your-api-server.com/api
```
## SM4 加密
系统默认使用 SM4 加密传输数据。如需调整:
1. 修改 `src/utils/sm4.js` 中的 `defaultKey`
2. 确保后端使用相同的密钥
如不需要加密,可以修改 `request.js` 中的请求拦截器。
## 路由说明
| 路径 | 页面 | 需要认证 |
|------|------|----------|
| /login | 登录页 | 否 |
| /home | 首页 | 是 |
| /dashboard | 数据大屏 | 是 |
| /404 | 404错误页 | 否 |
| /500 | 500错误页 | 否 |
## 静态资源使用
### 目录结构
```
public/ # 不经过构建处理的静态资源
├── imgs/ # 全局图片logo、背景图等
├── js/ # 第三方JS库
├── fonts/ # 字体文件
└── data/ # 静态JSON数据
src/assets/ # 需要构建处理的资源
├── imgs/ # 组件级图片
├── icons/ # SVG图标
└── styles/ # SCSS样式文件
```
### 使用示例
**public 目录资源(直接通过根路径访问):**
```vue
<template>
<img src="/imgs/logo.png" alt="logo">
</template>
<script>
import { getPublicImageUrl } from '@/utils/assets'
const logoUrl = getPublicImageUrl('imgs/logo.png')
</script>
```
**assets 目录资源(通过别名访问):**
```vue
<template>
<img src="@/assets/imgs/avatar.png" alt="avatar">
</template>
<script>
import { getAssetImageUrl } from '@/utils/assets'
const avatarUrl = getAssetImageUrl('imgs/avatar.png')
</script>
```
**加载静态JSON数据**
```javascript
import { loadJsonData } from '@/utils/assets'
const config = await loadJsonData('config.json')
```
## 项目结构
```
yun3v2/
├── index.html
├── package.json
├── vite.config.js
├── src/
│ ├── main.js
│ ├── App.vue
│ ├── views/
│ ├── utils/
│ ├── router/
│ └── styles/
└── README.md
```
## 技术栈
- Vue 3.4
- Vue Router 4.3
- Pinia 2.1
- Element Plus 2.6
- Axios 1.6
- Vite 5.1
- SCSS

116
TOMCAT_DEPLOY_README.md Normal file
View File

@ -0,0 +1,116 @@
# Tomcat 部署配置说明
## 问题原因
Vue Router 使用 history 模式,刷新页面时 Tomcat 会尝试查找对应的物理路径,导致 404 错误。
## 解决方案
### 方案一:配置 Tomcat rewrite 规则(推荐)
1. **下载 rewrite.jar**
- 下载 Tomcat rewrite 模块https://tomcat.apache.org/download-90.cgi
- 将 `rewrite.jar` 放到 `$CATALINA_HOME/lib` 目录
2. **创建 WEB-INF/rewrite.config**
`wdk/WEB-INF/` 目录下创建 `rewrite.config` 文件,内容如下:
```
# 所有非文件请求都转发到 index.html
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^.*$ /wdk/index.html [L]
```
3. **修改 WEB-INF/web.xml**
`wdk/WEB-INF/web.xml` 中添加 rewrite 过滤器:
```xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
<display-name>Yun3v2 Application</display-name>
<!-- Rewrite Filter -->
<filter>
<filter-name>rewriteFilter</filter-name>
<filter-class>org.apache.rewrite.RewriteFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>rewriteFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- Welcome File -->
<welcome-file-list>
<welcome-file>index.html</welcome-file>
</welcome-file-list>
</web-app>
```
### 方案二:使用 hash 模式(简单但 URL 不美观)
修改 `src/router/index.js`
```javascript
import { createRouter, createWebHashHistory } from 'vue-router'
const router = createRouter({
history: createWebHashHistory(), // 使用 hash 模式
routes
})
```
这样 URL 会变成:`http://localhost:8080/wdk/#/home`
### 方案三:配置 Tomcat 全局 error-page
`wdk/WEB-INF/web.xml` 中添加:
```xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
<display-name>Yun3v2 Application</display-name>
<!-- 404 错误页面重定向到 index.html -->
<error-page>
<error-code>404</error-code>
<location>/index.html</location>
</error-page>
<!-- Welcome File -->
<welcome-file-list>
<welcome-file>index.html</welcome-file>
</welcome-file-list>
</web-app>
```
## 部署步骤
1. **重新构建项目**
```bash
npm run build
```
2. **复制文件到 Tomcat**
```
dist/ → tomcat/webapps/wdk/
```
3. **创建 WEB-INF 目录和配置文件**
```
tomcat/webapps/wdk/WEB-INF/
├── web.xml
└── rewrite.config (如果使用方案一)
```
4. **重启 Tomcat**
## 推荐方案
- **生产环境**使用方案一rewrite 规则)
- **快速测试**使用方案三error-page
- **内网简单部署**使用方案二hash 模式)

16
index.html Normal file
View File

@ -0,0 +1,16 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>上海地铁运三运营有限公司数字监管平台</title>
<style>
* { margin: 0; padding: 0; }
html, body, #app { width: 100%; height: 100%; overflow: auto; }
</style>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 403 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

View File

@ -0,0 +1,21 @@
<template>
<section class="traffic-1">1</section>
</template>
<style lang="scss" scoped>
.traffic-1 {
position: absolute;
left: 21.33px;
top: 128px;
width: 861.53px;
height: 894px;
opacity: 1;
display: flex;
flex-direction: column;
background: url('/imgs/traffic/1-bg.png') no-repeat center center;
background-size: cover;
backdrop-filter: blur(10px);
}
</style>

View File

@ -0,0 +1,429 @@
<template>
<div class="traffic-2">
<!-- <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>
.traffic-2 {
position: absolute;
left: 892.45px;
top: 128px;
width: 787.46px;
height: 894px;
opacity: 1;
display: flex;
flex-direction: column;
background: url('/imgs/traffic/2-bg.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,15 @@
<template>
<section class="traffic-3">3</section>
</template>
<style lang="scss" scoped>
.traffic-3 {
position: absolute;
left: 1689px;
top: 128px;
width: 1250px;
height: 895px;
background: url('/imgs/traffic/3-bg.png') no-repeat center center;
background-size: cover;
}
</style>

View File

@ -0,0 +1,277 @@
<template>
<section class="traffic-4">
<div class="chart-shell">
<v-chart class="traffic-4__chart" :option="chartOption" autoresize />
</div>
</section>
</template>
<script setup>
import { computed } from 'vue'
import VChart from 'vue-echarts'
import * as echarts from 'echarts'
const hours = ['5', '7', '9', '11', '13', '15', '17', '19', '21', '23']
const topActual = [800, 900, 2100, 4050, 1500, 1450, 1200, 2300, 900, 900]
const topCompare = [1400, 1800, 3400, 4500, 2950, 2600, 2800, 4300, 4300, 4850]
const bottomActual = [4800, 4400, 3000, 1600, 3150, 3500, 3350, 1850, 1450, 1650]
const bottomCompare = [5400, 5300, 4900, 2200, 4700, 5000, 3850, 4700, 5350, 5350 ]
const chartOption = computed(() => ({
backgroundColor: 'transparent',
animationDuration: 900,
animationDurationUpdate: 700,
animationEasing: 'cubicOut',
grid: [
{
left: 0,
right: 16,
top: 15,
height: 225,
containLabel: true
},
{
left: 0,
right: 16,
bottom: 20,
height: 225,
containLabel: true
}
],
xAxis: [
{
type: 'category',
boundaryGap: false,
data: hours,
gridIndex: 0,
axisLine: {
show: true
},
axisTick: {
show: false
},
axisLabel: {
color: 'rgba(226, 239, 255, 0.95)',
fontSize: 13,
fontWeight: 600,
margin: 12
},
splitLine: {
show: false
}
},
{
type: 'category',
boundaryGap: false,
data: hours,
gridIndex: 1,
position: 'top',
axisLine: {
show: true
},
axisTick: {
show: false
},
axisLabel: {
show: false
},
splitLine: {
show: false
}
}
],
yAxis: [
{
type: 'value',
gridIndex: 0,
interval: 1000,
axisLine: {
show: false
},
axisTick: {
show: false
},
axisLabel: {
color: 'rgba(222, 236, 255, 0.9)',
fontSize: 12,
margin: 10
},
splitLine: {
lineStyle: {
color: 'rgba(225, 238, 255, 0.48)',
type: 'dashed',
width: 1.5,
dashOffset: 10
}
}
},
{
type: 'value',
gridIndex: 1,
interval: 1000,
inverse: true,
axisLine: {
show: false
},
axisTick: {
show: false
},
axisLabel: {
color: 'rgba(222, 236, 255, 0.9)',
fontSize: 12,
margin: 10
},
splitLine: {
lineStyle: {
color: 'rgba(225, 238, 255, 0.48)',
type: 'dashed',
width: 1.5,
dashOffset: 10
}
}
}
],
series: [
{
name: '进出站',
type: 'line',
xAxisIndex: 0,
yAxisIndex: 0,
data: topActual,
smooth: false,
symbol: 'circle',
symbolSize: 6,
z: 5,
lineStyle: {
color: '#44d9ff',
width: 4,
cap: 'round',
join: 'round',
shadowBlur: 6,
shadowColor: 'rgba(68, 217, 255, 0.25)'
},
itemStyle: {
color: '#ffffff',
borderColor: 'rgba(167, 244, 255, 0.55)',
borderWidth: 1,
shadowBlur: 10,
shadowColor: 'rgba(90, 224, 255, 0.48)'
},
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(42, 200, 255, 0.9)' },
{ offset: 0.55, color: 'rgba(28, 132, 222, 0.5)' },
{ offset: 1, color: 'rgba(20, 88, 188, 0.2)' }
])
}
},
{
name: '进出站对比',
type: 'line',
xAxisIndex: 0,
yAxisIndex: 0,
data: topCompare,
smooth: false,
symbol: 'none',
z: 4,
lineStyle: {
color: '#F5C10B',
width: 3,
cap: 'round',
join: 'round'
}
},
{
name: '换乘站',
type: 'line',
xAxisIndex: 1,
yAxisIndex: 1,
data: bottomActual,
smooth: false,
symbol: 'circle',
symbolSize: 6,
z: 5,
lineStyle: {
color: '#29ffd0',
width: 4,
cap: 'round',
join: 'round',
shadowBlur: 6,
shadowColor: 'rgba(41, 255, 208, 0.22)'
},
itemStyle: {
color: '#ffffff',
borderColor: 'rgba(180, 255, 238, 0.55)',
borderWidth: 1.5,
shadowBlur: 10,
shadowColor: 'rgba(93, 255, 220, 0.42)'
},
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(16, 172, 138, 0.2)' },
{ offset: 0.42, color: 'rgba(22, 202, 168, 0.5)' },
{ offset: 1, color: 'rgba(22, 224, 174, 0.9)' }
])
}
},
{
name: '换乘站对比',
type: 'line',
xAxisIndex: 1,
yAxisIndex: 1,
data: bottomCompare,
smooth: false,
symbol: 'none',
z: 4,
lineStyle: {
color: '#F5C10B',
width: 3,
cap: 'round',
join: 'round'
}
}
],
tooltip: {
trigger: 'axis',
backgroundColor: 'rgba(0, 0, 0, 0.75)',
textStyle: {
color: '#fff',
fontSize: 12
},
formatter: params => {
const { name, seriesName, value } = params[0]
return `${name}时<br>${seriesName}: ${value}`
}
}
}))
</script>
<style lang="scss" scoped>
.traffic-4 {
position: absolute;
left: 2948px;
top: 128px;
width: 892px;
height: 564.59px;
background: url('/imgs/traffic/4-bg.png') no-repeat center center;
background-size: cover;
.chart-shell {
position: absolute;
left: 56px;
right: 8px;
top: 66px;
bottom: 4px;
background: #041a4b;
}
&__chart {
width: 100%;
height: 100%;
}
}
</style>

View File

@ -0,0 +1,291 @@
<template>
<section class="traffic-5">
<div class="panel-body">
<div class="rank-column">
<div v-for="item in leftList" :key="item.rank" class="rank-card">
<div class="rank-card__top">
<div class="rank-info">
<span class="rank-no" :class="`rank-no--${item.rank}`">NO.{{ item.rank }}</span>
<span class="rank-name">{{ item.name }}</span>
</div>
<div class="rank-value">
<span class="rank-value__num">{{ item.value }}</span>
<img src="/imgs/traffic/5-item1.png" alt="" class="rank-value__icon">
</div>
</div>
<div class="rank-bar">
<div class="rank-bar__fill" :style="{ width: `${item.percent}%` }"></div>
</div>
</div>
</div>
<div class="summary-column">
<div v-for="item in rightTopList" :key="item.rank" class="rank-card rank-card--compact">
<div class="rank-card__top">
<div class="rank-info">
<span class="rank-no" :class="`rank-no--${item.rank}`">NO.{{ item.rank }}</span>
<span class="rank-name">{{ item.name }}</span>
</div>
<div class="rank-value">
<span class="rank-value__num">{{ item.value }}</span>
<img src="/imgs/traffic/5-item1.png" alt="" class="rank-value__icon">
</div>
</div>
<div class="rank-bar">
<div class="rank-bar__fill" :style="{ width: `${item.percent}%` }"></div>
</div>
</div>
<div v-for="card in summaryCards" :key="card.label" class="summary-card">
<div class="summary-card__icon">
<img src="/imgs/traffic/5-item.png" :alt="card.label" class="summary-card__icon-img">
</div>
<div class="summary-card__content">
<div class="summary-label">{{ card.label }}</div>
<div class="summary-value">
<span class="summary-value__num">{{ card.value }}</span>
<span class="summary-value__unit"></span>
<img src="/imgs/traffic/5-item1.png" alt="" class="rank-value__icon">
</div>
</div>
</div>
</div>
</div>
</section>
</template>
<script setup>
import { computed, ref } from 'vue'
const rankList = ref([
{ rank: 1, name: '安检方式及设备', value: 57, percent: 100 },
{ rank: 2, name: '求助', value: 52, percent: 71 },
{ rank: 3, name: '车站管理', value: 50, percent: 66 },
{ rank: 4, name: '安检方式及设备', value: 26, percent: 58 },
{ rank: 5, name: '寻物', value: 16, percent: 33 },
{ rank: 6, name: '客运组织', value: 10, percent: 36 },
{ rank: 7, name: '运营计划', value: 8, percent: 28 }
])
setTimeout(() => {
rankList.value[0].value = 82
rankList.value[0].percent = 50
}, 2000)
const leftList = computed(() => rankList.value.slice(0, 5))
const rightTopList = computed(() => rankList.value.slice(5, 7))
const summaryCards = ref([
{ label: '年度客诉总数', value: 282 },
{ label: '投诉A类', value: 282 }
])
</script>
<style lang="scss" scoped>
$card-bg: #0d2e71;
$text-main: #ffffff;
$text-soft: #f4fbff;
$text-muted: rgba(255, 255, 255, 0.7);
$rank-colors: (
1: #ff342d,
2: #ffca17,
3: #32dfff,
4: #32dfff,
5: #32dfff,
6: #32dfff,
7: #32dfff
);
@mixin card-shell($padding: null, $background: $card-bg) {
position: relative;
background: $background;
@if $padding !=null {
padding: $padding;
box-sizing: border-box;
}
}
.traffic-5 {
position: absolute;
left: 3850px;
top: 128px;
width: 930px;
height: 564.59px;
padding: 22px 18px 16px;
box-sizing: border-box;
background: url('/imgs/traffic/5-bg.png') no-repeat center center;
background-size: cover;
.panel-body {
position: absolute;
top: 70px;
right: 20px;
bottom: 20px;
left: 20px;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
}
.rank-column,
.summary-column {
display: flex;
flex-direction: column;
gap: 14px;
min-height: 0;
}
.rank-card {
@include card-shell(18px 18px 14px);
height: 84px;
box-shadow: none;
&--compact {
height: 84px;
}
&__top {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
}
.rank-info {
min-width: 0;
display: flex;
align-items: baseline;
gap: 20px;
}
.rank-no {
min-width: 74px;
font-size: 26px;
font-style: italic;
font-weight: 900;
line-height: 1;
letter-spacing: 1px;
text-transform: uppercase;
text-shadow: 0 0 12px rgba(0, 210, 255, 0.15);
@each $rank, $color in $rank-colors {
&--#{$rank} {
color: $color;
}
}
}
.rank-name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 22px;
font-weight: 500;
letter-spacing: 1px;
color: $text-soft;
}
.rank-value {
display: flex;
align-items: center;
gap: 12px;
padding-left: 12px;
flex-shrink: 0;
&__num {
font-size: 34px;
font-weight: 800;
line-height: 1;
color: $text-main;
}
&__icon {
display: block;
width: 18px;
height: 22px;
flex-shrink: 0;
object-fit: contain;
}
}
.rank-bar {
margin-top: 8px;
margin-left: 2px;
height: 6px;
overflow: visible;
border-radius: 999px;
background: transparent;
box-shadow: none;
&__fill {
height: 100%;
border-radius: 999px;
background: #18FEFE;
box-shadow: 0px 0px 4px 0px rgba(24, 254, 254, 0.6);
transition: width 0.8s ease;
}
}
.summary-card {
@include card-shell(16px 20px, $card-bg);
flex: 1;
min-height: 0;
display: grid;
grid-template-columns: 150px 1fr;
align-items: center;
box-shadow: none;
&__icon {
width: 124px;
height: 104px;
margin-left: 8px;
}
&__icon-img {
display: block;
width: 100%;
height: 100%;
object-fit: contain;
}
&__content {
display: flex;
flex-direction: column;
gap: 12px;
padding-left: 10px;
}
}
.summary-label {
font-size: 24px;
font-weight: 800;
letter-spacing: 1px;
color: #f6fbff;
}
.summary-value {
display: flex;
align-items: baseline;
gap: 10px;
&__num {
font-size: 38px;
font-weight: 800;
line-height: 1;
color: $text-main;
}
&__unit {
font-size: 16px;
color: $text-muted;
}
}
}
</style>

View File

@ -0,0 +1,17 @@
<template>
<section class="traffic-6">6</section>
</template>
<style lang="scss" scoped>
.traffic-6 {
position: absolute;
left: 16px;
top: 1052px;
width: 2923px;
height: 535px;
opacity: 1;
background: url('/imgs/traffic/6-bg.png') no-repeat center center;
background-size: cover;
}
</style>

View File

@ -0,0 +1,15 @@
<template>
<section class="traffic-7"></section>
</template>
<style lang="scss" scoped>
.traffic-7 {
position: absolute;
left: 2948px;
top: 707px;
width: 892px;
height: 880px;
background: url('/imgs/traffic/7-bg.png') no-repeat center center;
background-size: cover;
}
</style>

View File

@ -0,0 +1,17 @@
<template>
<section class="traffic-8"></section>
</template>
<style lang="scss" scoped>
.traffic-8 {
position: absolute;
left: 3853.1px;
top: 707px;
width: 934px;
height: 884.69px;
opacity: 1;
background: url('/imgs/traffic/8-bg.png') no-repeat center center;
background-size: cover;
}
</style>

View File

@ -1,123 +1,34 @@
<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>
<Traffic1 />
<Traffic2 />
<Traffic3 />
<Traffic4 />
<Traffic5 />
<Traffic6 />
<Traffic7 />
<Traffic8 />
</div>
</template>
<script setup>
import Traffic1 from './components/Traffic1.vue'
import Traffic2 from './components/Traffic2.vue'
import Traffic3 from './components/Traffic3.vue'
import Traffic4 from './components/Traffic4.vue'
import Traffic5 from './components/Traffic5.vue'
import Traffic6 from './components/Traffic6.vue'
import Traffic7 from './components/Traffic7.vue'
import Traffic8 from './components/Traffic8.vue'
</script>
<style lang="scss" scoped>
.traffic-container {
width: 4800px;
height: 1514px;
position: relative;
overflow: hidden;
}
.traffic-panel {
height: 1620px;
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);
left: 0;
bottom: 0;
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>