|
|
@@ -1,5 +1,12 @@
|
|
|
<template>
|
|
|
- <div ref="chartRef" class="chart-container"></div>
|
|
|
+ <!-- chart-scroll: 外层视口, overflow-x:auto 提供原生横滚条
|
|
|
+ chart-container: 内层 ECharts 容器, 宽度由 _recalcContentWidth 命令式写 chartRef.style.width
|
|
|
+ - stageCount ≤ 4: 内宽 = wrap 宽度, 与容器同宽不滚
|
|
|
+ - stageCount > 4: 内宽 = wrap × (realMax / S4End), 多出部分横滚露出 S5+
|
|
|
+ 注意: 不用 :style 绑定, 因为 Vue 响应式刷 DOM 是异步的, 会让 initChart 抓到旧宽度. -->
|
|
|
+ <div ref="scrollWrap" class="chart-scroll">
|
|
|
+ <div ref="chartRef" class="chart-container"></div>
|
|
|
+ </div>
|
|
|
</template>
|
|
|
|
|
|
<script>
|
|
|
@@ -84,7 +91,9 @@ export default {
|
|
|
compactScanLine: { type: Boolean, default: false }
|
|
|
},
|
|
|
data() {
|
|
|
- return { scaleFactor: 1, vScaleFactor: 1, internalTime: 0 };
|
|
|
+ // contentWidthPx: 内层 chart-container 的像素宽度记账 (autoScan follow 用)
|
|
|
+ // DOM 上的宽度由 _recalcContentWidth 命令式写 chartRef.style.width, 不走 :style 响应式
|
|
|
+ return { scaleFactor: 1, vScaleFactor: 1, internalTime: 0, contentWidthPx: 0 };
|
|
|
},
|
|
|
computed: {
|
|
|
activeTime() {
|
|
|
@@ -111,11 +120,22 @@ export default {
|
|
|
},
|
|
|
mounted() {
|
|
|
this.internalTime = this.currentTime;
|
|
|
- this.initChart();
|
|
|
- if (this.autoScan) this.startAutoScan();
|
|
|
+ // 先测量 wrap 算出 contentWidthPx, 再 initChart, 避免 ECharts 初始化时 chartRef 宽度为 0
|
|
|
+ this.$nextTick(() => {
|
|
|
+ this._recalcContentWidth();
|
|
|
+ this.initChart();
|
|
|
+ if (this.autoScan) this.startAutoScan();
|
|
|
+ this._setupWrapObserver();
|
|
|
+ this._setupWheelToHorizontal();
|
|
|
+ });
|
|
|
},
|
|
|
beforeDestroy() {
|
|
|
this.stopAutoScan();
|
|
|
+ if (this._wrapRO) { this._wrapRO.disconnect(); this._wrapRO = null; }
|
|
|
+ if (this._wheelHandler && this.$refs.scrollWrap) {
|
|
|
+ this.$refs.scrollWrap.removeEventListener('wheel', this._wheelHandler);
|
|
|
+ this._wheelHandler = null;
|
|
|
+ }
|
|
|
},
|
|
|
watch: {
|
|
|
currentTime() {
|
|
|
@@ -131,18 +151,101 @@ export default {
|
|
|
this.updateChart();
|
|
|
if (val && this.autoScan) { this.startAutoScan(); }
|
|
|
},
|
|
|
- phaseData: { deep: true, handler(newVal) { if (this.$_chart && newVal.length > 0) this.updateChart(); } },
|
|
|
+ // phaseData 变更 → 阶段数可能变 → contentWidthPx 要重算 (4→32 阶段切换时尤其关键)
|
|
|
+ phaseData: { deep: true, handler(newVal) {
|
|
|
+ if (this.$_chart && newVal.length > 0) {
|
|
|
+ this._recalcContentWidth();
|
|
|
+ this.updateChart();
|
|
|
+ }
|
|
|
+ } },
|
|
|
+ cycleLength() { this._recalcContentWidth(); },
|
|
|
showAxis() { this.updateChart(); },
|
|
|
showScanLineLabel() { this.updateChart(); }
|
|
|
},
|
|
|
methods: {
|
|
|
updateScale() {
|
|
|
- const el = this.$el;
|
|
|
- if (!el) return;
|
|
|
- this.scaleFactor = Math.max(0.5, el.clientWidth / 600);
|
|
|
+ // 横向 scale 基于 wrap (视口) 宽度而非 chartRef (内层) 宽度
|
|
|
+ // 否则横滚生效后 chartRef 变 N 倍宽, scaleFactor 也跟着膨胀, 字号/badge 失控
|
|
|
+ const wrap = this.$refs.scrollWrap || this.$el;
|
|
|
+ if (!wrap) return;
|
|
|
+ this.scaleFactor = Math.max(0.5, wrap.clientWidth / 600);
|
|
|
// 垂直 scale:以 80px 容器高度为 1x 基准;矮容器拉小,高容器轻微放大
|
|
|
// 给紧凑模式 markLine label 用,避免窄行 chart 中 badge 视觉过重
|
|
|
- this.vScaleFactor = Math.max(0.4, Math.min(2, el.clientHeight / 80));
|
|
|
+ this.vScaleFactor = Math.max(0.4, Math.min(2, wrap.clientHeight / 80));
|
|
|
+ },
|
|
|
+ // 阶段边界: 与 renderCustomItem 里的 stagePoints 算法严格一致 (track 0 的 green 起始时刻 + 头尾)
|
|
|
+ _computeStagePoints() {
|
|
|
+ const realMax = this.getMaxTime();
|
|
|
+ let pts = (this.phaseData || []).filter(p => p[0] === 0 && p[5] === 'green').map(p => p[1]);
|
|
|
+ if (!pts.includes(0)) pts.unshift(0);
|
|
|
+ pts.push(realMax);
|
|
|
+ return Array.from(new Set(pts)).sort((a, b) => a - b);
|
|
|
+ },
|
|
|
+ // 重算内层 chart-container 像素宽度并命令式写到 DOM:
|
|
|
+ // - 4 阶段以内 → 清空 inline style, fallback 到 CSS width:100% (与 wrap 同宽, 不滚)
|
|
|
+ // - 超 4 阶段 → inline width = wrap × (realMax / S4End), 触发 chart-scroll 横滚
|
|
|
+ // 直接 style.width = 避免 Vue 响应式 :style 异步刷 DOM 导致 initChart 抓到旧宽度
|
|
|
+ _recalcContentWidth() {
|
|
|
+ const wrap = this.$refs.scrollWrap;
|
|
|
+ const chartDom = this.$refs.chartRef;
|
|
|
+ if (!wrap || !chartDom) return;
|
|
|
+ const wrapW = wrap.clientWidth;
|
|
|
+ if (wrapW <= 0) return;
|
|
|
+ const realMax = this.getMaxTime();
|
|
|
+ const stagePoints = this._computeStagePoints();
|
|
|
+ const stageCount = Math.max(1, stagePoints.length - 1);
|
|
|
+ // stagePoints[4] = 第 4 阶段末端 (S4End); stageCount<=4 时无第 5 项, visibleEnd 直接取 realMax 不滚
|
|
|
+ const visibleEnd = stageCount <= 4 ? realMax : stagePoints[4];
|
|
|
+ let newWidth;
|
|
|
+ if (!visibleEnd || visibleEnd <= 0 || stageCount <= 4) {
|
|
|
+ newWidth = wrapW;
|
|
|
+ chartDom.style.width = ''; // 让 CSS width:100% 接管
|
|
|
+ } else {
|
|
|
+ const ratio = realMax / visibleEnd;
|
|
|
+ newWidth = Math.max(wrapW, Math.round(wrapW * ratio));
|
|
|
+ chartDom.style.width = newWidth + 'px';
|
|
|
+ }
|
|
|
+ this.contentWidthPx = newWidth;
|
|
|
+ },
|
|
|
+ // 监听 wrap 尺寸变化: 父容器宽度变了 (面板缩放/多窗口切换) 要重算 contentWidthPx
|
|
|
+ // 内层 chartRef 宽度变化由 echartsResize mixin 自己接管, 不重复观察
|
|
|
+ _setupWrapObserver() {
|
|
|
+ if (this._wrapRO) this._wrapRO.disconnect();
|
|
|
+ const wrap = this.$refs.scrollWrap;
|
|
|
+ if (!wrap) return;
|
|
|
+ let timer = null;
|
|
|
+ this._wrapRO = new ResizeObserver(() => {
|
|
|
+ if (timer) clearTimeout(timer);
|
|
|
+ timer = setTimeout(() => requestAnimationFrame(() => this._recalcContentWidth()), 50);
|
|
|
+ });
|
|
|
+ this._wrapRO.observe(wrap);
|
|
|
+ },
|
|
|
+ // 鼠标滚轮纵滚 → 横滚转换: 普通鼠标只有 deltaY, 不接管的话用户在 chart 区域滚轮无任何反馈
|
|
|
+ // 触摸板 deltaX 已是横向, 也透传; passive:false 才能 preventDefault 阻止页面纵向滚动
|
|
|
+ _setupWheelToHorizontal() {
|
|
|
+ const wrap = this.$refs.scrollWrap;
|
|
|
+ if (!wrap) return;
|
|
|
+ this._wheelHandler = (e) => {
|
|
|
+ if (wrap.scrollWidth <= wrap.clientWidth) return; // 不可滚时不接管, 让事件正常冒泡
|
|
|
+ const delta = e.deltaY !== 0 ? e.deltaY : e.deltaX;
|
|
|
+ if (delta === 0) return;
|
|
|
+ e.preventDefault();
|
|
|
+ wrap.scrollLeft += delta;
|
|
|
+ };
|
|
|
+ wrap.addEventListener('wheel', this._wheelHandler, { passive: false });
|
|
|
+ },
|
|
|
+ // autoScan 时让扫描线跟着滚动条走: scan line 落到视口中心位置 (clamp 到合法 scrollLeft 范围)
|
|
|
+ // 不滚条 (contentWidthPx <= wrapW) 时直接 return
|
|
|
+ _followScanLine() {
|
|
|
+ const wrap = this.$refs.scrollWrap;
|
|
|
+ if (!wrap || this.contentWidthPx <= 0) return;
|
|
|
+ const wrapW = wrap.clientWidth;
|
|
|
+ if (this.contentWidthPx <= wrapW) return;
|
|
|
+ const realMax = this.getMaxTime();
|
|
|
+ if (!realMax) return;
|
|
|
+ const scanX = this.contentWidthPx * (this.activeTime / realMax);
|
|
|
+ const target = Math.max(0, Math.min(this.contentWidthPx - wrapW, scanX - wrapW / 2));
|
|
|
+ wrap.scrollLeft = target;
|
|
|
},
|
|
|
// 扫描线 label 配置:紧凑模式跟随 min(横向, 纵向) scale 自适应,否则保持原始(10px font, [4,8] padding)
|
|
|
_scanLabel(realMaxTime) {
|
|
|
@@ -194,6 +297,8 @@ export default {
|
|
|
if (this.clipToActive) this.updateChart();
|
|
|
else this.updateScanLine();
|
|
|
}
|
|
|
+ // 横滚生效时让 scan line 自动居中可见 (4 阶段以内 _followScanLine 内部会 return)
|
|
|
+ this._followScanLine();
|
|
|
this.$emit('scan-tick', this.internalTime);
|
|
|
};
|
|
|
joinGlobalTimer(this._scanListener);
|
|
|
@@ -518,7 +623,30 @@ export default {
|
|
|
</script>
|
|
|
|
|
|
<style scoped>
|
|
|
-.chart-container {
|
|
|
- width: 100%; height: 100%; flex: 1; min-height: 0; overflow: hidden;
|
|
|
+/* 外层视口: 提供原生横滚, 4 阶段以内不滚 (内容宽 == 视口宽) 也不预留滚条占位 */
|
|
|
+.chart-scroll {
|
|
|
+ width: 100%; height: 100%; flex: 1;
|
|
|
+ min-width: 0; min-height: 0;
|
|
|
+ overflow-x: auto; overflow-y: hidden;
|
|
|
+ /* Firefox 常驻细条 */
|
|
|
+ scrollbar-width: thin;
|
|
|
+ scrollbar-color: rgba(161, 190, 255, 0.45) rgba(255, 255, 255, 0.04);
|
|
|
+}
|
|
|
+/* WebKit/Blink (Chrome/Edge/Electron) 常驻细条: 防 overlay scrollbar 隐身让用户以为不能滚 */
|
|
|
+.chart-scroll::-webkit-scrollbar { height: 6px; }
|
|
|
+.chart-scroll::-webkit-scrollbar-track {
|
|
|
+ background: rgba(255, 255, 255, 0.04);
|
|
|
+ border-radius: 3px;
|
|
|
+}
|
|
|
+.chart-scroll::-webkit-scrollbar-thumb {
|
|
|
+ background: rgba(161, 190, 255, 0.45);
|
|
|
+ border-radius: 3px;
|
|
|
+}
|
|
|
+.chart-scroll::-webkit-scrollbar-thumb:hover {
|
|
|
+ background: rgba(161, 190, 255, 0.75);
|
|
|
+}
|
|
|
+/* 内层 chart 容器: 默认 100%, 阶段>4 时 JS 命令式写 style.width (大于 wrap), 触发横滚 */
|
|
|
+.chart-container {
|
|
|
+ width: 100%; height: 100%;
|
|
|
}
|
|
|
</style>
|