|
@@ -5,60 +5,22 @@
|
|
|
<script>
|
|
<script>
|
|
|
import * as echarts from 'echarts';
|
|
import * as echarts from 'echarts';
|
|
|
|
|
|
|
|
|
|
+// 定义大屏设计稿基准宽度 (假设为 1920)
|
|
|
|
|
+const DESIGN_WIDTH = 1920;
|
|
|
|
|
+
|
|
|
export default {
|
|
export default {
|
|
|
name: 'TrafficTimeSpace',
|
|
name: 'TrafficTimeSpace',
|
|
|
props: {
|
|
props: {
|
|
|
- // 路口名称数组(从起点到终点)
|
|
|
|
|
- intersections: {
|
|
|
|
|
- type: Array,
|
|
|
|
|
- required: true
|
|
|
|
|
- },
|
|
|
|
|
- // 各路口到起点的距离(米),与 intersections 一一对应
|
|
|
|
|
- distances: {
|
|
|
|
|
- type: Array,
|
|
|
|
|
- required: true
|
|
|
|
|
- },
|
|
|
|
|
- // 绿波带数据: [{ yBottom, yTop, xBL, xBR, xTL, xTR, label, direction }]
|
|
|
|
|
- // direction: 'up' | 'down'
|
|
|
|
|
- waveData: {
|
|
|
|
|
- type: Array,
|
|
|
|
|
- default: () => []
|
|
|
|
|
- },
|
|
|
|
|
- // 绿灯数据: [{ y, start, end }]
|
|
|
|
|
- greenData: {
|
|
|
|
|
- type: Array,
|
|
|
|
|
- default: () => []
|
|
|
|
|
- },
|
|
|
|
|
- // 可视窗口显示的时间跨度(秒)
|
|
|
|
|
- viewWindow: {
|
|
|
|
|
- type: Number,
|
|
|
|
|
- default: 350
|
|
|
|
|
- },
|
|
|
|
|
- // 是否启用自动滚动
|
|
|
|
|
- autoScroll: {
|
|
|
|
|
- type: Boolean,
|
|
|
|
|
- default: false
|
|
|
|
|
- },
|
|
|
|
|
- // 自动滚动速度(秒/帧)
|
|
|
|
|
- scrollSpeed: {
|
|
|
|
|
- type: Number,
|
|
|
|
|
- default: 0.5
|
|
|
|
|
- },
|
|
|
|
|
- // 上行波带颜色
|
|
|
|
|
- upWaveColor: {
|
|
|
|
|
- type: String,
|
|
|
|
|
- default: 'rgba(46, 196, 182, 0.45)'
|
|
|
|
|
- },
|
|
|
|
|
- // 下行波带颜色
|
|
|
|
|
- downWaveColor: {
|
|
|
|
|
- type: String,
|
|
|
|
|
- default: 'rgba(104, 231, 95, 0.4)'
|
|
|
|
|
- },
|
|
|
|
|
- // 波带文字颜色
|
|
|
|
|
- waveLabelColor: {
|
|
|
|
|
- type: String,
|
|
|
|
|
- default: '#e0f7fa'
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ intersections: { type: Array, required: true },
|
|
|
|
|
+ distances: { type: Array, required: true },
|
|
|
|
|
+ waveData: { type: Array, default: () => [] },
|
|
|
|
|
+ greenData: { type: Array, default: () => [] },
|
|
|
|
|
+ viewWindow: { type: Number, default: 350 },
|
|
|
|
|
+ autoScroll: { type: Boolean, default: false },
|
|
|
|
|
+ scrollSpeed: { type: Number, default: 0.5 },
|
|
|
|
|
+ upWaveColor: { type: String, default: 'rgba(46, 196, 182, 0.45)' },
|
|
|
|
|
+ downWaveColor: { type: String, default: 'rgba(104, 231, 95, 0.4)' },
|
|
|
|
|
+ waveLabelColor: { type: String, default: '#e0f7fa' }
|
|
|
},
|
|
},
|
|
|
data() {
|
|
data() {
|
|
|
return {
|
|
return {
|
|
@@ -68,35 +30,19 @@ export default {
|
|
|
};
|
|
};
|
|
|
},
|
|
},
|
|
|
computed: {
|
|
computed: {
|
|
|
- maxDistance() {
|
|
|
|
|
- return Math.max(...this.distances);
|
|
|
|
|
- },
|
|
|
|
|
|
|
+ maxDistance() { return Math.max(...this.distances); },
|
|
|
maxDataTime() {
|
|
maxDataTime() {
|
|
|
let max = 0;
|
|
let max = 0;
|
|
|
- this.waveData.forEach(w => {
|
|
|
|
|
- max = Math.max(max, w.xBR, w.xTR);
|
|
|
|
|
- });
|
|
|
|
|
- this.greenData.forEach(g => {
|
|
|
|
|
- max = Math.max(max, g.end);
|
|
|
|
|
- });
|
|
|
|
|
|
|
+ this.waveData.forEach(w => max = Math.max(max, w.xBR, w.xTR));
|
|
|
|
|
+ this.greenData.forEach(g => max = Math.max(max, g.end));
|
|
|
return max;
|
|
return max;
|
|
|
},
|
|
},
|
|
|
- // 转为 ECharts custom series 所需的数组格式
|
|
|
|
|
echartsWaveData() {
|
|
echartsWaveData() {
|
|
|
- return this.waveData.map(w => [
|
|
|
|
|
- w.yBottom, w.yTop, w.xBL, w.xBR, w.xTL, w.xTR, w.label || '', w.direction || 'up'
|
|
|
|
|
- ]);
|
|
|
|
|
|
|
+ return this.waveData.map(w => [w.yBottom, w.yTop, w.xBL, w.xBR, w.xTL, w.xTR, w.label || '', w.direction || 'up']);
|
|
|
},
|
|
},
|
|
|
- echartsGreenData() {
|
|
|
|
|
- return this.greenData.map(g => [g.y, g.start, g.end]);
|
|
|
|
|
- },
|
|
|
|
|
- echartsRedData() {
|
|
|
|
|
- return this.distances.map(y => [y]);
|
|
|
|
|
- },
|
|
|
|
|
- // 路口名称需要反转以匹配 Y 轴方向
|
|
|
|
|
- reversedIntersections() {
|
|
|
|
|
- return [...this.intersections].reverse();
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ echartsGreenData() { return this.greenData.map(g => [g.y, g.start, g.end]); },
|
|
|
|
|
+ echartsRedData() { return this.distances.map(y => [y]); },
|
|
|
|
|
+ reversedIntersections() { return [...this.intersections].reverse(); }
|
|
|
},
|
|
},
|
|
|
mounted() {
|
|
mounted() {
|
|
|
this.$nextTick(() => {
|
|
this.$nextTick(() => {
|
|
@@ -119,17 +65,27 @@ export default {
|
|
|
watch: {
|
|
watch: {
|
|
|
waveData() { this.updateChart(); },
|
|
waveData() { this.updateChart(); },
|
|
|
greenData() { this.updateChart(); },
|
|
greenData() { this.updateChart(); },
|
|
|
- autoScroll(val) {
|
|
|
|
|
- val ? this.startScroll() : this.stopScroll();
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ autoScroll(val) { val ? this.startScroll() : this.stopScroll(); }
|
|
|
},
|
|
},
|
|
|
methods: {
|
|
methods: {
|
|
|
|
|
+ // 【新增】:获取真实缩放比例
|
|
|
|
|
+ getRealScale() {
|
|
|
|
|
+ return window.innerWidth / DESIGN_WIDTH;
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
initChart() {
|
|
initChart() {
|
|
|
this.chart = echarts.init(this.$refs.chartContainer);
|
|
this.chart = echarts.init(this.$refs.chartContainer);
|
|
|
- this._resizeHandler = () => this.chart && this.chart.resize();
|
|
|
|
|
|
|
+
|
|
|
|
|
+ // 窗口变化时的防抖处理
|
|
|
|
|
+ this._resizeHandler = () => {
|
|
|
|
|
+ if (this.chart) {
|
|
|
|
|
+ this.chart.resize();
|
|
|
|
|
+ this.updateChart(); // 【关键】:尺寸变了,需要重新生成 option 刷新字体和线条粗细!
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
window.addEventListener('resize', this._resizeHandler);
|
|
window.addEventListener('resize', this._resizeHandler);
|
|
|
|
|
|
|
|
- // 监听容器尺寸变化(弹窗拉伸、延迟渲染等场景)
|
|
|
|
|
|
|
+ // 容器变化时的处理 (拖拽拉伸弹窗)
|
|
|
if (typeof ResizeObserver !== 'undefined') {
|
|
if (typeof ResizeObserver !== 'undefined') {
|
|
|
this._roaPending = false;
|
|
this._roaPending = false;
|
|
|
this._resizeObserver = new ResizeObserver(() => {
|
|
this._resizeObserver = new ResizeObserver(() => {
|
|
@@ -137,7 +93,10 @@ export default {
|
|
|
this._roaPending = true;
|
|
this._roaPending = true;
|
|
|
requestAnimationFrame(() => {
|
|
requestAnimationFrame(() => {
|
|
|
this._roaPending = false;
|
|
this._roaPending = false;
|
|
|
- this.chart && this.chart.resize();
|
|
|
|
|
|
|
+ if (this.chart) {
|
|
|
|
|
+ this.chart.resize();
|
|
|
|
|
+ this.updateChart(); // 【关键】:同样需要重新渲染内部配置
|
|
|
|
|
+ }
|
|
|
});
|
|
});
|
|
|
}
|
|
}
|
|
|
});
|
|
});
|
|
@@ -154,17 +113,30 @@ export default {
|
|
|
const distances = this.distances;
|
|
const distances = this.distances;
|
|
|
const intersections = this.reversedIntersections;
|
|
const intersections = this.reversedIntersections;
|
|
|
const maxDist = this.maxDistance;
|
|
const maxDist = this.maxDistance;
|
|
|
|
|
+
|
|
|
|
|
+ // 拿到当前缩放比例
|
|
|
|
|
+ const scale = this.getRealScale();
|
|
|
|
|
|
|
|
this.chart.setOption({
|
|
this.chart.setOption({
|
|
|
backgroundColor: 'transparent',
|
|
backgroundColor: 'transparent',
|
|
|
animation: false,
|
|
animation: false,
|
|
|
tooltip: { show: false },
|
|
tooltip: { show: false },
|
|
|
- grid: { left: 110, right: 30, top: 40, bottom: 40 },
|
|
|
|
|
|
|
+ // 网格的边距也乘一下,防止屏幕缩小时文字被切掉
|
|
|
|
|
+ grid: {
|
|
|
|
|
+ left: Math.round(90 * scale),
|
|
|
|
|
+ right: Math.round(15 * scale),
|
|
|
|
|
+ top: Math.round(10 * scale),
|
|
|
|
|
+ bottom: Math.round(30 * scale)
|
|
|
|
|
+ },
|
|
|
xAxis: {
|
|
xAxis: {
|
|
|
type: 'value',
|
|
type: 'value',
|
|
|
min: this.currentViewTime,
|
|
min: this.currentViewTime,
|
|
|
max: this.currentViewTime + this.viewWindow,
|
|
max: this.currentViewTime + this.viewWindow,
|
|
|
- axisLabel: { color: '#7b95b9', formatter: '{value}s', fontSize: 13 },
|
|
|
|
|
|
|
+ axisLabel: {
|
|
|
|
|
+ color: '#7b95b9',
|
|
|
|
|
+ formatter: '{value}s',
|
|
|
|
|
+ fontSize: Math.round(10 * scale) // 【修复】坐标轴字体随比例缩放
|
|
|
|
|
+ },
|
|
|
splitLine: { show: true, lineStyle: { color: '#1a305d', type: 'solid' } },
|
|
splitLine: { show: true, lineStyle: { color: '#1a305d', type: 'solid' } },
|
|
|
axisLine: { lineStyle: { color: '#31548e' } }
|
|
axisLine: { lineStyle: { color: '#31548e' } }
|
|
|
},
|
|
},
|
|
@@ -177,7 +149,7 @@ export default {
|
|
|
interval: 0,
|
|
interval: 0,
|
|
|
color: '#9cb1d4',
|
|
color: '#9cb1d4',
|
|
|
fontWeight: 'bold',
|
|
fontWeight: 'bold',
|
|
|
- fontSize: 13,
|
|
|
|
|
|
|
+ fontSize: Math.round(10 * scale), // 【修复】路口名字体随比例缩放
|
|
|
formatter: value => distances.includes(value) ? intersections[distances.indexOf(value)] : ''
|
|
formatter: value => distances.includes(value) ? intersections[distances.indexOf(value)] : ''
|
|
|
},
|
|
},
|
|
|
splitLine: { show: true, lineStyle: { color: '#1a305d' } }
|
|
splitLine: { show: true, lineStyle: { color: '#1a305d' } }
|
|
@@ -185,27 +157,21 @@ export default {
|
|
|
series: [
|
|
series: [
|
|
|
{
|
|
{
|
|
|
type: 'custom',
|
|
type: 'custom',
|
|
|
- renderItem: function (params, api) {
|
|
|
|
|
- return self.renderWave(params, api);
|
|
|
|
|
- },
|
|
|
|
|
|
|
+ renderItem: function (params, api) { return self.renderWave(params, api, scale); },
|
|
|
data: this.echartsWaveData,
|
|
data: this.echartsWaveData,
|
|
|
clip: true,
|
|
clip: true,
|
|
|
z: 1
|
|
z: 1
|
|
|
},
|
|
},
|
|
|
{
|
|
{
|
|
|
type: 'custom',
|
|
type: 'custom',
|
|
|
- renderItem: function (params, api) {
|
|
|
|
|
- return self.renderRedBackground(params, api);
|
|
|
|
|
- },
|
|
|
|
|
|
|
+ renderItem: function (params, api) { return self.renderRedBackground(params, api, scale); },
|
|
|
data: this.echartsRedData,
|
|
data: this.echartsRedData,
|
|
|
clip: true,
|
|
clip: true,
|
|
|
z: 2
|
|
z: 2
|
|
|
},
|
|
},
|
|
|
{
|
|
{
|
|
|
type: 'custom',
|
|
type: 'custom',
|
|
|
- renderItem: function (params, api) {
|
|
|
|
|
- return self.renderGreenLight(params, api);
|
|
|
|
|
- },
|
|
|
|
|
|
|
+ renderItem: function (params, api) { return self.renderGreenLight(params, api, scale); },
|
|
|
data: this.echartsGreenData,
|
|
data: this.echartsGreenData,
|
|
|
clip: true,
|
|
clip: true,
|
|
|
z: 3
|
|
z: 3
|
|
@@ -214,29 +180,38 @@ export default {
|
|
|
});
|
|
});
|
|
|
},
|
|
},
|
|
|
|
|
|
|
|
- renderRedBackground(params, api) {
|
|
|
|
|
|
|
+ // 注意:这里的 renderItem 都把 scale 参数传进去了
|
|
|
|
|
+ renderRedBackground(params, api, scale) {
|
|
|
const y = api.value(0);
|
|
const y = api.value(0);
|
|
|
const startX = api.coord([0, y])[0];
|
|
const startX = api.coord([0, y])[0];
|
|
|
const endX = api.coord([this.maxDataTime, y])[0];
|
|
const endX = api.coord([this.maxDataTime, y])[0];
|
|
|
|
|
+ // 高度和 Y轴偏移量 都要乘上 scale
|
|
|
|
|
+ const rectHeight = Math.round(6 * scale);
|
|
|
|
|
+ const rectOffsetY = Math.round(3 * scale);
|
|
|
|
|
+
|
|
|
return {
|
|
return {
|
|
|
type: 'rect',
|
|
type: 'rect',
|
|
|
- shape: { x: startX, y: api.coord([0, y])[1] - 3, width: endX - startX, height: 6 },
|
|
|
|
|
|
|
+ shape: { x: startX, y: api.coord([0, y])[1] - rectOffsetY, width: endX - startX, height: rectHeight },
|
|
|
style: { fill: '#f02828' }
|
|
style: { fill: '#f02828' }
|
|
|
};
|
|
};
|
|
|
},
|
|
},
|
|
|
|
|
|
|
|
- renderGreenLight(params, api) {
|
|
|
|
|
|
|
+ renderGreenLight(params, api, scale) {
|
|
|
const y = api.value(0);
|
|
const y = api.value(0);
|
|
|
const p1 = api.coord([api.value(1), y]);
|
|
const p1 = api.coord([api.value(1), y]);
|
|
|
const p2 = api.coord([api.value(2), y]);
|
|
const p2 = api.coord([api.value(2), y]);
|
|
|
|
|
+ // 高度和 Y轴偏移量 都要乘上 scale
|
|
|
|
|
+ const rectHeight = Math.round(8 * scale);
|
|
|
|
|
+ const rectOffsetY = Math.round(4 * scale);
|
|
|
|
|
+
|
|
|
return {
|
|
return {
|
|
|
type: 'rect',
|
|
type: 'rect',
|
|
|
- shape: { x: p1[0], y: p1[1] - 4, width: p2[0] - p1[0], height: 8 },
|
|
|
|
|
|
|
+ shape: { x: p1[0], y: p1[1] - rectOffsetY, width: p2[0] - p1[0], height: rectHeight },
|
|
|
style: api.style({ fill: '#68e75f' })
|
|
style: api.style({ fill: '#68e75f' })
|
|
|
};
|
|
};
|
|
|
},
|
|
},
|
|
|
|
|
|
|
|
- renderWave(params, api) {
|
|
|
|
|
|
|
+ renderWave(params, api, scale) {
|
|
|
const yBottom = api.value(0), yTop = api.value(1);
|
|
const yBottom = api.value(0), yTop = api.value(1);
|
|
|
const xBL = api.value(2), xBR = api.value(3), xTL = api.value(4), xTR = api.value(5);
|
|
const xBL = api.value(2), xBR = api.value(3), xTL = api.value(4), xTR = api.value(5);
|
|
|
const text = api.value(6), dir = api.value(7);
|
|
const text = api.value(6), dir = api.value(7);
|
|
@@ -246,6 +221,9 @@ export default {
|
|
|
const angle = -Math.atan2(ptTL[1] - ptBL[1], ptTL[0] - ptBL[0]);
|
|
const angle = -Math.atan2(ptTL[1] - ptBL[1], ptTL[0] - ptBL[0]);
|
|
|
const fillColor = dir === 'up' ? this.upWaveColor : this.downWaveColor;
|
|
const fillColor = dir === 'up' ? this.upWaveColor : this.downWaveColor;
|
|
|
|
|
|
|
|
|
|
+ // 动态字体大小
|
|
|
|
|
+ const fontSize = Math.round(12 * scale);
|
|
|
|
|
+
|
|
|
return {
|
|
return {
|
|
|
type: 'group',
|
|
type: 'group',
|
|
|
children: [
|
|
children: [
|
|
@@ -264,7 +242,7 @@ export default {
|
|
|
style: {
|
|
style: {
|
|
|
text: text,
|
|
text: text,
|
|
|
fill: this.waveLabelColor,
|
|
fill: this.waveLabelColor,
|
|
|
- font: 'bold 15px sans-serif',
|
|
|
|
|
|
|
+ font: `bold ${fontSize}px sans-serif`, // 【修复】绿波带上的说明文字按比例缩放
|
|
|
textAlign: 'center',
|
|
textAlign: 'center',
|
|
|
textVerticalAlign: 'middle'
|
|
textVerticalAlign: 'middle'
|
|
|
}
|
|
}
|
|
@@ -281,6 +259,7 @@ export default {
|
|
|
this.currentViewTime = 0;
|
|
this.currentViewTime = 0;
|
|
|
}
|
|
}
|
|
|
if (this.chart) {
|
|
if (this.chart) {
|
|
|
|
|
+ // 这里仅仅更新 X 轴视图,非常轻量
|
|
|
this.chart.setOption({
|
|
this.chart.setOption({
|
|
|
xAxis: { min: this.currentViewTime, max: this.currentViewTime + this.viewWindow }
|
|
xAxis: { min: this.currentViewTime, max: this.currentViewTime + this.viewWindow }
|
|
|
});
|
|
});
|
|
@@ -296,7 +275,10 @@ export default {
|
|
|
},
|
|
},
|
|
|
|
|
|
|
|
resize() {
|
|
resize() {
|
|
|
- if (this.chart) this.chart.resize();
|
|
|
|
|
|
|
+ if (this.chart) {
|
|
|
|
|
+ this.chart.resize();
|
|
|
|
|
+ this.updateChart(); // 对外暴露的 resize 方法也加上重新渲染配置
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
};
|
|
};
|
|
@@ -307,4 +289,4 @@ export default {
|
|
|
width: 100%;
|
|
width: 100%;
|
|
|
height: 100%;
|
|
height: 100%;
|
|
|
}
|
|
}
|
|
|
-</style>
|
|
|
|
|
|
|
+</style>
|