|
|
@@ -5,7 +5,17 @@
|
|
|
<!-- 频率范围 -->
|
|
|
<div class="box full-row">
|
|
|
<div class="panel-block">
|
|
|
- <span class="label1">频率</span>
|
|
|
+ <span class="label1">坐标轴</span>
|
|
|
+ <el-select
|
|
|
+ v-model="axisMode"
|
|
|
+ size="mini"
|
|
|
+ style="width: 120px"
|
|
|
+ @change="handleAxisModeChange"
|
|
|
+ >
|
|
|
+ <el-option label="频率(Hz)" value="hz" />
|
|
|
+ <el-option label="阶次(Order)" value="order" />
|
|
|
+ </el-select>
|
|
|
+ <span class="label1">{{ axisModeLabel }}</span>
|
|
|
<el-input v-model="freqMin" size="mini" placeholder="下限" />
|
|
|
<span>~</span>
|
|
|
<el-input v-model="freqMax" size="mini" placeholder="上限" />
|
|
|
@@ -13,18 +23,21 @@
|
|
|
应用
|
|
|
</el-button>
|
|
|
</div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- <div class="control-panel">
|
|
|
- <!-- 手动标注 -->
|
|
|
- <div class="panel-block">
|
|
|
- <span class="label1">标注</span>
|
|
|
- <el-input v-model="manualFreq" size="mini" placeholder="频率" />
|
|
|
- <el-input v-model="multiple" size="mini" placeholder="倍频" />
|
|
|
- <el-button size="mini" type="success" @click="handleMark"
|
|
|
- >标注</el-button
|
|
|
- >
|
|
|
- <el-button size="mini" type="info" @click="removeMark">清除</el-button>
|
|
|
+ <!-- 手动标注 -->
|
|
|
+ <div class="panel-block">
|
|
|
+ <span class="label1">标注</span>
|
|
|
+ <el-input v-model="manualFreq" size="mini" placeholder="频率/阶次" />
|
|
|
+ <el-input v-model="multiple" size="mini" placeholder="倍频" />
|
|
|
+ <el-button size="mini" type="success" @click="handleMark"
|
|
|
+ >标注</el-button
|
|
|
+ >
|
|
|
+ <el-button size="mini" type="info" @click="removeMark"
|
|
|
+ >清除</el-button
|
|
|
+ >
|
|
|
+ </div>
|
|
|
+ <p class="hint-text">
|
|
|
+ 提示:点击某一采样时刻对应的谱线可高亮该时间(红色);再点同一条谱线取消高亮。
|
|
|
+ </p>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
@@ -49,17 +62,26 @@ export default {
|
|
|
freqMax: "",
|
|
|
manualFreq: "",
|
|
|
multiple: 1,
|
|
|
+ axisMode: "hz", // hz | order
|
|
|
|
|
|
freqRange: null, // 当前频率范围
|
|
|
markLines: [], // 标注线
|
|
|
+ /** 点击高亮的采样时间(时间戳 ms),null 表示未选中 */
|
|
|
+ highlightedTime: null,
|
|
|
};
|
|
|
},
|
|
|
+ computed: {
|
|
|
+ axisModeLabel() {
|
|
|
+ return this.axisMode === "order" ? "阶次" : "频率";
|
|
|
+ },
|
|
|
+ },
|
|
|
|
|
|
watch: {
|
|
|
data: {
|
|
|
deep: true,
|
|
|
immediate: true,
|
|
|
handler() {
|
|
|
+ this.highlightedTime = null;
|
|
|
this.$nextTick(() => {
|
|
|
this.renderChart();
|
|
|
});
|
|
|
@@ -67,7 +89,42 @@ export default {
|
|
|
},
|
|
|
},
|
|
|
|
|
|
+ beforeDestroy() {
|
|
|
+ const el = this.$refs.chart;
|
|
|
+ if (el && typeof el.removeAllListeners === "function") {
|
|
|
+ el.removeAllListeners("plotly_click");
|
|
|
+ el.removeAllListeners("plotly_relayout");
|
|
|
+ }
|
|
|
+ if (el && typeof Plotly.purge === "function") {
|
|
|
+ Plotly.purge(el);
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
methods: {
|
|
|
+ calcOrder(freq, rpm) {
|
|
|
+ const f = Number(freq);
|
|
|
+ const r = Number(rpm);
|
|
|
+ if (!Number.isFinite(f) || !Number.isFinite(r) || r <= 0) return null;
|
|
|
+ return (60 * f) / r;
|
|
|
+ },
|
|
|
+ toAxisValue(freq, rpm) {
|
|
|
+ if (this.axisMode === "order") {
|
|
|
+ return this.calcOrder(freq, rpm);
|
|
|
+ }
|
|
|
+ const f = Number(freq);
|
|
|
+ return Number.isFinite(f) ? f : null;
|
|
|
+ },
|
|
|
+ handleAxisModeChange() {
|
|
|
+ // 单位切换后,旧范围/标注的单位语义失效,清空避免误读
|
|
|
+ this.freqRange = null;
|
|
|
+ this.freqMin = "";
|
|
|
+ this.freqMax = "";
|
|
|
+ this.markLines = [];
|
|
|
+ this.manualFreq = "";
|
|
|
+ this.multiple = 1;
|
|
|
+ this.highlightedTime = null;
|
|
|
+ this.renderChart();
|
|
|
+ },
|
|
|
/** ✅ 时间解析(核心修复) */
|
|
|
parseTime(time) {
|
|
|
if (typeof time === "number") {
|
|
|
@@ -88,6 +145,18 @@ export default {
|
|
|
d.getDate(),
|
|
|
)} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
|
|
},
|
|
|
+
|
|
|
+ /** ✅ 轴刻度:仅显示 月/日 时:分 */
|
|
|
+ formatTimeTick(t) {
|
|
|
+ const d = new Date(t);
|
|
|
+ const pad = (n) => String(n).padStart(2, "0");
|
|
|
+ const md = `${pad(d.getMonth() + 1)}/${pad(d.getDate())}`;
|
|
|
+ const hh = pad(d.getHours());
|
|
|
+ const mm = pad(d.getMinutes());
|
|
|
+ // 00:00 不展示时间(避免“0点”刷屏)
|
|
|
+ if (hh === "00" && mm === "00") return md;
|
|
|
+ return `${md} ${hh}:${mm}`;
|
|
|
+ },
|
|
|
renderChart() {
|
|
|
if (!this.data.length) return;
|
|
|
|
|
|
@@ -96,7 +165,7 @@ export default {
|
|
|
========================== */
|
|
|
const groupMap = new Map();
|
|
|
|
|
|
- this.data.forEach(([freq, amp, time, Hz]) => {
|
|
|
+ this.data.forEach(([freq, amp, time, Hz, RPM]) => {
|
|
|
const t = this.parseTime(time);
|
|
|
const key = `${t}_${Hz}`;
|
|
|
|
|
|
@@ -108,7 +177,7 @@ export default {
|
|
|
});
|
|
|
}
|
|
|
|
|
|
- groupMap.get(key).points.push([freq, amp]);
|
|
|
+ groupMap.get(key).points.push([freq, amp, RPM]);
|
|
|
});
|
|
|
|
|
|
/** =========================
|
|
|
@@ -117,6 +186,13 @@ export default {
|
|
|
const groups = [...groupMap.values()];
|
|
|
// .sort((a, b) => a.time - b.time);
|
|
|
|
|
|
+ if (
|
|
|
+ this.highlightedTime != null &&
|
|
|
+ !groups.some((g) => g.time === this.highlightedTime)
|
|
|
+ ) {
|
|
|
+ this.highlightedTime = null;
|
|
|
+ }
|
|
|
+
|
|
|
/** =========================
|
|
|
* 3. 同时间分组(关键!!!)
|
|
|
========================== */
|
|
|
@@ -133,6 +209,13 @@ export default {
|
|
|
* 4. traces
|
|
|
========================== */
|
|
|
const traces = [];
|
|
|
+ let maxAmp = 0;
|
|
|
+
|
|
|
+ // ✅ 时间轴等间隔:用索引轴 + ticktext(避免时间戳间隔不均导致视觉“挤/拉”)
|
|
|
+ const uniqueTimes = Array.from(new Set(groups.map((g) => g.time))).sort(
|
|
|
+ (a, b) => a - b,
|
|
|
+ );
|
|
|
+ const timeToIndex = new Map(uniqueTimes.map((t, i) => [t, i]));
|
|
|
|
|
|
groups.forEach((group, globalIndex) => {
|
|
|
const { time, Hz, points } = group;
|
|
|
@@ -141,32 +224,42 @@ export default {
|
|
|
const index = sameTimeGroups.indexOf(group);
|
|
|
const total = sameTimeGroups.length;
|
|
|
|
|
|
- const spread = 200; // 👈 控制展开宽度(可调)
|
|
|
-
|
|
|
- // 👉 同时间“对称展开”
|
|
|
- const offsetX = total > 1 ? (index - (total - 1) / 2) * spread : 0;
|
|
|
+ // 同一时间点若存在多组(不同采样频率 Hz),用极小偏移避免完全重叠(不影响刻度显示)
|
|
|
+ const offsetX = total > 1 ? (index - (total - 1) / 2) * 0.08 : 0;
|
|
|
+ const xBase = timeToIndex.get(time) ?? 0;
|
|
|
|
|
|
const x = [];
|
|
|
const y = [];
|
|
|
const z = [];
|
|
|
const text = [];
|
|
|
+ const lineColor = [];
|
|
|
|
|
|
points.sort((a, b) => a[0] - b[0]);
|
|
|
|
|
|
- points.forEach(([freq, amp]) => {
|
|
|
+ points.forEach(([freq, amp, rpm]) => {
|
|
|
+ const axisY = this.toAxisValue(freq, rpm);
|
|
|
+ if (axisY == null) return;
|
|
|
if (this.freqRange) {
|
|
|
- if (freq < this.freqRange.min || freq > this.freqRange.max) return;
|
|
|
+ if (axisY < this.freqRange.min || axisY > this.freqRange.max)
|
|
|
+ return;
|
|
|
}
|
|
|
|
|
|
- x.push(time + offsetX); // 👈 关键
|
|
|
- y.push(freq);
|
|
|
- z.push(amp + globalIndex * 0.02); // 防Z重叠
|
|
|
+ // 轴映射:X=采样时间(等间隔),Y=频率(Hz),Z=幅值
|
|
|
+ x.push(xBase + offsetX);
|
|
|
+ y.push(axisY);
|
|
|
+ z.push(amp);
|
|
|
+ if (Number.isFinite(amp)) {
|
|
|
+ maxAmp = Math.max(maxAmp, Number(amp));
|
|
|
+ }
|
|
|
+ lineColor.push(amp);
|
|
|
|
|
|
+ const order = this.calcOrder(freq, rpm);
|
|
|
text.push(
|
|
|
`时间: ${this.formatTime(time)}<br>` +
|
|
|
`采样频率: ${Hz} Hz<br>` +
|
|
|
`频率: ${freq.toFixed(2)} Hz<br>` +
|
|
|
- `幅值: ${amp.toFixed(3)}`,
|
|
|
+ `阶次: ${order == null ? "-" : order.toFixed(3)}<br>` +
|
|
|
+ `加速度(m/s^2): ${amp.toFixed(3)}`,
|
|
|
);
|
|
|
});
|
|
|
|
|
|
@@ -177,35 +270,48 @@ export default {
|
|
|
text,
|
|
|
type: "scatter3d",
|
|
|
mode: "lines",
|
|
|
- opacity: 0.7,
|
|
|
- line: {
|
|
|
- width: 2,
|
|
|
- color: "#162961",
|
|
|
- },
|
|
|
+ opacity: 0.78,
|
|
|
+ line:
|
|
|
+ this.highlightedTime != null && time === this.highlightedTime
|
|
|
+ ? {
|
|
|
+ width: 3,
|
|
|
+ color: "#ff0000",
|
|
|
+ }
|
|
|
+ : {
|
|
|
+ width: 2,
|
|
|
+ color: lineColor,
|
|
|
+ colorscale: "Viridis",
|
|
|
+ cauto: true,
|
|
|
+ },
|
|
|
showlegend: false,
|
|
|
hovertemplate: "%{text}<extra></extra>",
|
|
|
});
|
|
|
});
|
|
|
|
|
|
+ this._dataTraceCount = groups.length;
|
|
|
+ this._traceIndexToTime = groups.map((g) => g.time);
|
|
|
+
|
|
|
/** =========================
|
|
|
- * 5. X轴刻度(时间 + Hz)
|
|
|
+ * 5. 采样时间刻度(仅 月/日 时:分)
|
|
|
========================== */
|
|
|
- const tickvals = groups.map((g) => g.time);
|
|
|
-
|
|
|
- const ticktext = groups.map((g) => {
|
|
|
- return `${this.formatTime(g.time)}-${g.Hz}Hz`;
|
|
|
- });
|
|
|
+ const tickvals = uniqueTimes.map((_, i) => i);
|
|
|
+ const ticktext = uniqueTimes.map((t) => this.formatTimeTick(t));
|
|
|
+ // 仅用于 z 轴标签:去掉 0,避免原点出现两个“0”标签
|
|
|
+ const zTickVals =
|
|
|
+ maxAmp > 0
|
|
|
+ ? Array.from({ length: 5 }, (_, i) => ((i + 1) * maxAmp) / 5)
|
|
|
+ : [];
|
|
|
|
|
|
/** =========================
|
|
|
* 6. 标注线
|
|
|
========================== */
|
|
|
if (this.markLines.length) {
|
|
|
- const minTime = Math.min(...groups.map((g) => g.time));
|
|
|
- const maxTime = Math.max(...groups.map((g) => g.time));
|
|
|
+ const minX = 0;
|
|
|
+ const maxX = Math.max(0, uniqueTimes.length - 1);
|
|
|
|
|
|
this.markLines.forEach((line) => {
|
|
|
traces.push({
|
|
|
- x: [minTime, maxTime],
|
|
|
+ x: [minX, maxX],
|
|
|
y: [line.freq, line.freq],
|
|
|
z: [0, 0],
|
|
|
type: "scatter3d",
|
|
|
@@ -228,49 +334,92 @@ export default {
|
|
|
========================== */
|
|
|
const layout = {
|
|
|
// title: "3D 瀑布频谱图",
|
|
|
- paper_bgcolor: "#f5f7fa",
|
|
|
- margin: { l: 0, r: 0, b: 0, t: 40 },
|
|
|
+ paper_bgcolor: "#ffffff",
|
|
|
+ plot_bgcolor: "#ffffff",
|
|
|
+ margin: { l: 0, r: 0, b: 10, t: 26 },
|
|
|
|
|
|
scene: {
|
|
|
xaxis: {
|
|
|
- autorange: "reversed",
|
|
|
- title: "时间",
|
|
|
+ // title: "采样时间",
|
|
|
+ title: "",
|
|
|
tickvals,
|
|
|
ticktext,
|
|
|
- tickangle: 30,
|
|
|
- gridcolor: "#fff",
|
|
|
- backgroundcolor: "#e0e7f1",
|
|
|
+ tickangle: 45,
|
|
|
+ gridcolor: "#e5e5e5",
|
|
|
+ zeroline: false,
|
|
|
+ showspikes: false,
|
|
|
+ ticks: "outside",
|
|
|
+ tickfont: { size: 10, color: "#606266" },
|
|
|
+ titlefont: { size: 10, color: "#606266" },
|
|
|
+ backgroundcolor: "#ffffff",
|
|
|
showbackground: true,
|
|
|
+ showline: true,
|
|
|
+ // linecolor: "#dcdfe6",
|
|
|
+ linecolor: "#e5e5e5",
|
|
|
+ linewidth: 1,
|
|
|
},
|
|
|
yaxis: {
|
|
|
- title: "频率 (Hz)",
|
|
|
- gridcolor: "#fff",
|
|
|
- backgroundcolor: "#e0e7f1",
|
|
|
+ title: this.axisMode === "order" ? "阶次 (Order)" : "频率 (Hz)",
|
|
|
+ gridcolor: "#e5e5e5",
|
|
|
+ zeroline: false,
|
|
|
+ showspikes: false,
|
|
|
+ ticks: "outside",
|
|
|
+ tickfont: { size: 9, color: "#606266" },
|
|
|
+ titlefont: { size: 10, color: "#606266" },
|
|
|
+ backgroundcolor: "#fafafa",
|
|
|
showbackground: true,
|
|
|
+ showline: true,
|
|
|
+ // linecolor: "#dcdfe6",
|
|
|
+ linecolor: "#e5e5e5",
|
|
|
+ linewidth: 1,
|
|
|
+ // tickangle: 90,
|
|
|
},
|
|
|
zaxis: {
|
|
|
- title: "幅值",
|
|
|
- gridcolor: "#fff",
|
|
|
- backgroundcolor: "#e0e7f1",
|
|
|
+ title: "",
|
|
|
+ gridcolor: "#e5e5e5",
|
|
|
+ zeroline: false,
|
|
|
+ showspikes: false,
|
|
|
+ ticks: "outside",
|
|
|
+ tickangle: 90,
|
|
|
+ tickmode: "array",
|
|
|
+ tickvals: zTickVals,
|
|
|
+ tickformat: ".2f",
|
|
|
+ tickfont: { size: 10, color: "#606266" },
|
|
|
+ titlefont: {
|
|
|
+ size: 10,
|
|
|
+ color: "#606266",
|
|
|
+ },
|
|
|
+ backgroundcolor: "#fbfbfb",
|
|
|
showbackground: true,
|
|
|
+ showline: true,
|
|
|
+ // linecolor: "#dcdfe6",
|
|
|
+ linecolor: "#e5e5e5",
|
|
|
+ linewidth: 1,
|
|
|
},
|
|
|
aspectmode: "manual",
|
|
|
- aspectratio: { x: 2.0, y: 2.8, z: 1.2 },
|
|
|
+ // X 采用等间隔索引后,整体更均衡
|
|
|
+ aspectratio: { x: 2.4, y: 3.2, z: 1.1 },
|
|
|
+ // annotations: [
|
|
|
+ // {
|
|
|
+ // text: "加速度(m/s^2)",
|
|
|
+ // x: 0.92, // 右上角位置可微调
|
|
|
+ // y: 0.98,
|
|
|
+ // xref: "paper",
|
|
|
+ // yref: "paper",
|
|
|
+ // showarrow: false,
|
|
|
+ // font: { size: 10, color: "#606266" },
|
|
|
+ // align: "right",
|
|
|
+ // },
|
|
|
+ // ],
|
|
|
camera: {
|
|
|
- up: {
|
|
|
- x: -0.1644947035315824,
|
|
|
- y: -0.07969781808287146,
|
|
|
- z: 0.9831529638377166,
|
|
|
- },
|
|
|
- center: {
|
|
|
- x: -0.052807476121180814,
|
|
|
- y: 0.02451796399554085,
|
|
|
- z: -0.022911006648570736,
|
|
|
- },
|
|
|
+ up: { x: 0, y: 0, z: 1 },
|
|
|
+ center: { x: 0, y: 0, z: 0 },
|
|
|
+ // 让时间轴视觉更贴近底边
|
|
|
+ // eye: { x: 2.6, y: 1.2, z: 0.8 },
|
|
|
eye: {
|
|
|
- x: 2.980700714870927,
|
|
|
- y: 1.6273671421077383,
|
|
|
- z: 0.6145682420564063,
|
|
|
+ x: 2.8547815750982184,
|
|
|
+ y: 0.3847735823828088,
|
|
|
+ z: 0.7363229242526919,
|
|
|
},
|
|
|
projection: {
|
|
|
type: "orthographic",
|
|
|
@@ -280,234 +429,63 @@ export default {
|
|
|
};
|
|
|
|
|
|
/** =========================
|
|
|
- * 8. 渲染
|
|
|
+ * 8. 渲染(先卸监听再 react,避免重绘过程中 plotly_click 重入 renderChart 栈溢出)
|
|
|
========================== */
|
|
|
- Plotly.react(this.$refs.chart, traces, layout, {
|
|
|
+ const plotEl = this.$refs.chart;
|
|
|
+ if (!plotEl) return;
|
|
|
+
|
|
|
+ if (typeof plotEl.removeAllListeners === "function") {
|
|
|
+ plotEl.removeAllListeners("plotly_relayout");
|
|
|
+ plotEl.removeAllListeners("plotly_click");
|
|
|
+ }
|
|
|
+
|
|
|
+ const plotOpts = {
|
|
|
responsive: true,
|
|
|
- displayModeBar: false,
|
|
|
+ displayModeBar: true,
|
|
|
+ displaylogo: false,
|
|
|
+ modeBarButtonsToRemove: [
|
|
|
+ "zoom3d",
|
|
|
+ "pan3d",
|
|
|
+ "orbitRotation",
|
|
|
+ "tableRotation",
|
|
|
+ "resetCameraDefault3d",
|
|
|
+ "resetCameraLastSave3d",
|
|
|
+ "hoverClosest3d",
|
|
|
+ ],
|
|
|
+ };
|
|
|
+
|
|
|
+ const done = Plotly.react(plotEl, traces, layout, plotOpts);
|
|
|
+ const bind = () => this._bindPlotlyEvents();
|
|
|
+ if (done && typeof done.then === "function") {
|
|
|
+ done.then(bind).catch(bind);
|
|
|
+ } else {
|
|
|
+ this.$nextTick(bind);
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ _bindPlotlyEvents() {
|
|
|
+ const plotElement = this.$refs.chart;
|
|
|
+ if (!plotElement || typeof plotElement.on !== "function") return;
|
|
|
+
|
|
|
+ plotElement.on("plotly_relayout", (eventData) => {
|
|
|
+ if (eventData && eventData["scene.camera"]) {
|
|
|
+ console.log("当前相机视角:", eventData["scene.camera"]);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ plotElement.on("plotly_click", (ev) => {
|
|
|
+ const pt = ev.points && ev.points[0];
|
|
|
+ if (!pt || pt.curveNumber == null) return;
|
|
|
+ const n = this._dataTraceCount || 0;
|
|
|
+ if (pt.curveNumber >= n) return;
|
|
|
+ const t =
|
|
|
+ this._traceIndexToTime && this._traceIndexToTime[pt.curveNumber];
|
|
|
+ if (t == null) return;
|
|
|
+ this.highlightedTime = this.highlightedTime === t ? null : t;
|
|
|
+ this.$nextTick(() => {
|
|
|
+ this.renderChart();
|
|
|
+ });
|
|
|
});
|
|
|
},
|
|
|
- // renderChart() {
|
|
|
- // if (!this.data.length) return;
|
|
|
-
|
|
|
- // /** =========================
|
|
|
- // * 1. 分组
|
|
|
- // ========================== */
|
|
|
- // // console.log(this.data, "groupMap");
|
|
|
- // const groupMap = new Map();
|
|
|
-
|
|
|
- // this.data.forEach(([freq, amp, time, Hz]) => {
|
|
|
- // const t = this.parseTime(time);
|
|
|
- // const key = `${t}_${Hz}`; // 👈 用key区分
|
|
|
-
|
|
|
- // if (!groupMap.has(key)) {
|
|
|
- // groupMap.set(key, {
|
|
|
- // time: t,
|
|
|
- // Hz,
|
|
|
- // points: [],
|
|
|
- // });
|
|
|
- // }
|
|
|
-
|
|
|
- // groupMap.get(key).points.push([freq, amp]);
|
|
|
- // });
|
|
|
-
|
|
|
- // /** =========================
|
|
|
- // * 2. 排序
|
|
|
- // ========================== */
|
|
|
- // const times = [...groupMap.keys()].sort((a, b) => a - b);
|
|
|
-
|
|
|
- // /** =========================
|
|
|
- // * 3. traces
|
|
|
- // ========================== */
|
|
|
- // const traces = [];
|
|
|
- // times.forEach((time, index) => {
|
|
|
- // const points = groupMap.get(time);
|
|
|
-
|
|
|
- // points.sort((a, b) => a[0] - b[0]);
|
|
|
-
|
|
|
- // const x = [];
|
|
|
- // const y = [];
|
|
|
- // const z = [];
|
|
|
- // const text = [];
|
|
|
-
|
|
|
- // // const offset = index * 0.02;
|
|
|
-
|
|
|
- // points.forEach(([freq, amp]) => {
|
|
|
- // if (this.freqRange) {
|
|
|
- // if (freq < this.freqRange.min || freq > this.freqRange.max) return;
|
|
|
- // }
|
|
|
- // const offsetX = index * 50; // 👈 新增
|
|
|
- // const offsetZ = index * 0.02; // 👈 你已有
|
|
|
- // x.push(time + offsetX);
|
|
|
- // y.push(freq);
|
|
|
- // z.push(amp + offsetZ);
|
|
|
-
|
|
|
- // text.push(
|
|
|
- // `时间: ${this.formatTime(time)}<br>` +
|
|
|
- // `频率: ${freq.toFixed(2)} Hz<br>` +
|
|
|
- // `幅值: ${amp.toFixed(3)}`,
|
|
|
- // );
|
|
|
- // });
|
|
|
-
|
|
|
- // traces.push({
|
|
|
- // x,
|
|
|
- // y,
|
|
|
- // z,
|
|
|
- // text,
|
|
|
- // type: "scatter3d",
|
|
|
- // mode: "lines",
|
|
|
- // opacity: 0.7,
|
|
|
- // line: {
|
|
|
- // width: 2,
|
|
|
- // color: "#162961",
|
|
|
- // },
|
|
|
- // showlegend: false,
|
|
|
- // hovertemplate: "%{text}<extra></extra>",
|
|
|
- // });
|
|
|
- // });
|
|
|
- // /** =========================
|
|
|
- // * ✅ 标注线(修复重复问题)
|
|
|
- // ========================== */
|
|
|
- // if (this.markLines.length) {
|
|
|
- // const minTime = Math.min(...times);
|
|
|
- // const maxTime = Math.max(...times);
|
|
|
-
|
|
|
- // this.markLines.forEach((line) => {
|
|
|
- // traces.push({
|
|
|
- // x: [minTime, maxTime],
|
|
|
- // y: [line.freq, line.freq],
|
|
|
- // z: [0, 0],
|
|
|
- // type: "scatter3d",
|
|
|
- // mode: "lines+text",
|
|
|
- // line: {
|
|
|
- // color: "#ff0000",
|
|
|
- // width: 4,
|
|
|
- // dash: "dash",
|
|
|
- // },
|
|
|
- // text: ["", line.label],
|
|
|
-
|
|
|
- // textposition: "top right",
|
|
|
- // showlegend: false,
|
|
|
- // hovertemplate: `${line.label}<extra></extra>`,
|
|
|
- // });
|
|
|
- // });
|
|
|
- // }
|
|
|
-
|
|
|
- // // 👉 抽稀刻度(防止重叠)
|
|
|
- // const step = Math.ceil(times.length / 8);
|
|
|
- // const tickvals = times.filter((_, i) => i % step === 0);
|
|
|
- // const ticktext = tickvals.map(this.formatTime);
|
|
|
-
|
|
|
- // /** =========================
|
|
|
- // * 5. 布局(工业风)
|
|
|
- // ========================== */
|
|
|
- // const layout = {
|
|
|
- // title: "3D 瀑布频谱图",
|
|
|
- // paper_bgcolor: "#f5f7fa",
|
|
|
- // margin: { l: 0, r: 0, b: 0, t: 40 },
|
|
|
-
|
|
|
- // scene: {
|
|
|
- // xaxis: {
|
|
|
- // autorange: "reversed", //轴方向
|
|
|
- // title: "时间",
|
|
|
- // tickvals,
|
|
|
- // ticktext,
|
|
|
- // dtick: "D3",
|
|
|
- // // showbackground: true,
|
|
|
- // // backgroundcolor: "#ffffff",
|
|
|
- // // gridcolor: "#e0e7f1",
|
|
|
- // gridcolor: "#fff",
|
|
|
- // backgroundcolor: "#e0e7f1",
|
|
|
- // showbackground: true,
|
|
|
- // linecolor: "black",
|
|
|
- // ticks: "outside",
|
|
|
- // ticklen: 10,
|
|
|
- // tickcolor: "black",
|
|
|
- // zeroline: false,
|
|
|
- // tickangle: -10,
|
|
|
- // tickangle: 30, // 👈 倾斜
|
|
|
- // margin: {
|
|
|
- // l: 20,
|
|
|
- // r: 120, // 👉 关键!!给时间留空间
|
|
|
- // t: 40,
|
|
|
- // b: 40,
|
|
|
- // },
|
|
|
- // },
|
|
|
- // yaxis: {
|
|
|
- // title: "频率 (Hz)",
|
|
|
- // gridcolor: "#fff",
|
|
|
- // backgroundcolor: "#e0e7f1",
|
|
|
- // showbackground: true,
|
|
|
- // linecolor: "black",
|
|
|
- // ticks: "outside",
|
|
|
- // ticklen: 10,
|
|
|
- // tickcolor: "black",
|
|
|
- // zeroline: false,
|
|
|
- // tickangle: -10,
|
|
|
- // tickangle: 0,
|
|
|
- // },
|
|
|
- // zaxis: {
|
|
|
- // title: "幅值",
|
|
|
- // gridcolor: "#fff",
|
|
|
- // backgroundcolor: "#e0e7f1",
|
|
|
- // showbackground: true,
|
|
|
- // linecolor: "black",
|
|
|
- // ticks: "outside",
|
|
|
- // ticklen: 10,
|
|
|
- // tickcolor: "black",
|
|
|
- // zeroline: false,
|
|
|
- // tickangle: -10,
|
|
|
- // tickangle: 0,
|
|
|
- // },
|
|
|
- // /** ✅ 核心1:比例(决定“扁不扁”) */
|
|
|
- // // bgcolor: "#e5ecf6",
|
|
|
- // // gridcolor: "#fff",
|
|
|
- // aspectmode: "manual",
|
|
|
- // aspectratio: { x: 2.0, y: 2.8, z: 1.2 },
|
|
|
- // /** ✅ 核心2:相机(决定轴在哪边) */
|
|
|
- // camera: {
|
|
|
- // up: {
|
|
|
- // x: -0.1644947035315824,
|
|
|
- // y: -0.07969781808287146,
|
|
|
- // z: 0.9831529638377166,
|
|
|
- // },
|
|
|
- // center: {
|
|
|
- // x: -0.052807476121180814,
|
|
|
- // y: 0.02451796399554085,
|
|
|
- // z: -0.022911006648570736,
|
|
|
- // },
|
|
|
- // eye: {
|
|
|
- // x: 2.980700714870927,
|
|
|
- // y: 1.6273671421077383,
|
|
|
- // z: 0.6145682420564063,
|
|
|
- // },
|
|
|
- // projection: {
|
|
|
- // type: "orthographic",
|
|
|
- // },
|
|
|
- // },
|
|
|
- // },
|
|
|
- // };
|
|
|
-
|
|
|
- // /** =========================
|
|
|
- // * 6. 渲染
|
|
|
- // ========================== */
|
|
|
- // Plotly.react(this.$refs.chart, traces, layout, {
|
|
|
- // responsive: true,
|
|
|
- // displayModeBar: false,
|
|
|
- // });
|
|
|
- // // 监听图表的 relayout 事件,获取并输出相机视角
|
|
|
- // const plotElement = document.getElementById(`waterfall-chart`);
|
|
|
- // plotElement.on("plotly_relayout", function (eventData) {
|
|
|
- // // 在每次布局变更时,打印当前相机视角
|
|
|
- // if (eventData["scene.camera"]) {
|
|
|
- // console.log(
|
|
|
- // "当前相机视角:",
|
|
|
- // eventData["scene.camera"],
|
|
|
- // eventData["scene.aspectratio"],
|
|
|
- // );
|
|
|
- // }
|
|
|
- // });
|
|
|
- // },
|
|
|
removeMark() {
|
|
|
this.markLines = [];
|
|
|
this.manualFreq = "";
|
|
|
@@ -533,9 +511,13 @@ export default {
|
|
|
const newLines = [];
|
|
|
|
|
|
for (let i = 1; i <= multi; i++) {
|
|
|
+ const val = base * i;
|
|
|
newLines.push({
|
|
|
- freq: base * i,
|
|
|
- label: `${i}x (${base * i}Hz)`,
|
|
|
+ freq: val,
|
|
|
+ label:
|
|
|
+ this.axisMode === "order"
|
|
|
+ ? `${i}x (${val}Order)`
|
|
|
+ : `${i}x (${val}Hz)`,
|
|
|
});
|
|
|
}
|
|
|
|
|
|
@@ -567,9 +549,17 @@ export default {
|
|
|
.full-row {
|
|
|
width: 100%;
|
|
|
.panel-block {
|
|
|
- width: 45%;
|
|
|
+ width: 65%;
|
|
|
+ margin: 5px 0;
|
|
|
}
|
|
|
}
|
|
|
+.hint-text {
|
|
|
+ width: 100%;
|
|
|
+ margin: 4px 0 0;
|
|
|
+ font-size: 12px;
|
|
|
+ color: #909399;
|
|
|
+ line-height: 1.45;
|
|
|
+}
|
|
|
.panel-block {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
@@ -583,8 +573,9 @@ export default {
|
|
|
.label1 {
|
|
|
font-size: 12px;
|
|
|
color: #666;
|
|
|
- display: inline-block;
|
|
|
- width: 75px;
|
|
|
+ /* 父级是 flex:仅写 width 会被 flex-shrink 压缩,需固定 flex 基准且不收缩 */
|
|
|
+ flex: 0 0 40px;
|
|
|
+ box-sizing: border-box;
|
|
|
}
|
|
|
|
|
|
.btn-group {
|
|
|
@@ -630,7 +621,8 @@ export default {
|
|
|
gap: 6px;
|
|
|
}
|
|
|
|
|
|
+/* 勿用 flex:1,否则会拉伸子项,内联 width 无法按预期生效 */
|
|
|
.el-select {
|
|
|
- flex: 1;
|
|
|
+ flex: 0 0 auto;
|
|
|
}
|
|
|
</style>
|