Commit 6acd0460 by zhaopanyu

zpy

parent 344e397f
...@@ -42,3 +42,12 @@ export function delJsdb(jh) { ...@@ -42,3 +42,12 @@ export function delJsdb(jh) {
method: 'delete' method: 'delete'
}) })
} }
// 井身结构图(按井号返回结构绘图数据)
export function getJsjgt(params) {
return request({
url: '/system/jsdb/jsjgt',
method: 'get',
params
})
}
<template>
<div class="chart-container">
<div class="chart-layout">
<div id="mainzftdj" class="chart" ref="chartRef"></div>
<aside v-if="legendItems && legendItems.length" class="strata-legend">
<div class="legend-header">
<span>层位图例</span>
<el-button class="export-btn" @click="exportChart" :disabled="loading || !myChart" title="导出图表为图片"
icon="el-icon-download" size="small">
导出
</el-button>
</div>
<div class="legend-list">
<div v-for="(item, index) in legendItems" :key="item.name || index" class="legend-item">
<div class="legend-icon" :style="getLegendSwatchStyle(item)"></div>
<span class="legend-label">{{ item.name || '-' }}</span>
</div>
</div>
</aside>
</div>
<div v-if="loading" class="loading-overlay">
<div class="loading-spinner"></div>
<span>加载中...</span>
</div>
</div>
</template>
<script>
import * as echarts from "echarts";
import { getdjZft } from "@/api/optimization/initialization";
import { getljqxData } from "@/api/system/cjsjLas";
import { listLjSssjSd } from "@/api/optimization/ljSssjSd";
import { text } from "d3";
export default {
name: "HistogramGraph",
props: {
jh: {
type: String,
default: "",
},
jhs: {
type: String,
default: ''
},
// 美化配置选项
theme: {
type: String,
default: "modern", // modern, elegant, vibrant
validator: value => ["modern", "elegant", "vibrant"].includes(value)
}
},
data() {
return {
mockData: {},
legendItems: [],
myChart: null,
initRetryCount: 0,
maxRetryCount: 5,
resizeObserver: null,
loading: false,
debounceTimer: null,
lastStackedAreas: null,
lastChartConfig: null,
lastXAxisLabels: null,
lastDepthIntervals: null,
currentGraphicElements: [],
// 录井多曲线数据(钻时/扭矩/立压/钻压/转速/泵冲/入口流量)
curveData: null,
};
},
computed: {
// 根据主题获取颜色配置
colorScheme() {
const schemes = {
modern: {
primary: "#3B82F6",
secondary: "#10B981",
accent: "#F59E0B",
background: "#F8FAFC",
text: "#1F2937",
border: "#E5E7EB",
gradient: {
start: "#3B82F6",
end: "#1D4ED8"
}
},
elegant: {
primary: "#6366F1",
secondary: "#8B5CF6",
accent: "#EC4899",
background: "#FAFAFA",
text: "#374151",
border: "#D1D5DB",
gradient: {
start: "#6366F1",
end: "#4F46E5"
}
},
vibrant: {
primary: "#EF4444",
secondary: "#06B6D4",
accent: "#F97316",
background: "#FEFEFE",
text: "#111827",
border: "#F3F4F6",
gradient: {
start: "#EF4444",
end: "#DC2626"
}
}
};
return schemes[this.theme];
},
legendPanelWidth() {
return this.legendItems && this.legendItems.length ? 260 : 0;
}
},
watch: {
jh: {
handler(newVal) {
// 不自动刷新,等待父级触发 loadData
},
immediate: false
},
legendItems: {
handler() {
this.$nextTick(() => {
const chartDom = this.$refs.chartRef;
if (chartDom) {
this.setChartDimensions(chartDom);
this.performResize();
}
});
},
deep: true
}
},
mounted() {
// 初始化空图表,不拉数据;等待父组件触发 loadData
this.initChart();
this.setupEventListeners();
},
beforeDestroy() {
this.cleanup();
},
methods: {
// 初始化图表
async initChart() {
try {
this.loading = true;
const chartDom = this.$refs.chartRef;
if (!chartDom) {
throw new Error("未找到图表容器 DOM");
}
this.setChartDimensions(chartDom);
// 重试机制优化
if (this.shouldRetry()) {
this.scheduleRetry();
return;
}
this.resetRetryCount();
this.disposeChart();
this.createChart(chartDom);
// 并行获取直方图数据 + 多曲线数据
const [mockData, curveData] = await Promise.all([
this.getList(),
this.getCurveData()
]);
this.curveData = curveData;
if (!this.hasValidData(mockData)) {
this.renderEmpty(chartDom);
return;
}
await this.renderRealData(mockData);
} catch (error) {
console.error("图表初始化失败:", error);
this.handleError(error);
} finally {
this.loading = false;
}
},
// 设置图表尺寸
setChartDimensions(chartDom) {
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
const containerWidth = this.$el?.clientWidth || viewportWidth;
const rect = this.$el?.getBoundingClientRect();
const topOffset = rect ? rect.top : 0;
const legendWidth = this.legendPanelWidth || 0;
const containerPadding = 20; // chart-container 水平 padding 之和
const panelGap = legendWidth ? 5 : 0; // 与右侧图例的间距
const safetyMargin = 12; // 额外预留
const widthPadding = containerPadding + panelGap + legendWidth + safetyMargin;
const availableWidth = Math.max(360, containerWidth - widthPadding);
const verticalPadding = 40;
const heightByViewport = viewportHeight - topOffset - verticalPadding;
const fallbackHeight = this.$el?.clientHeight || viewportHeight;
const availableHeight = Math.max(360, heightByViewport, fallbackHeight - 20);
chartDom.style.width = `${availableWidth}px`;
chartDom.style.height = `${availableHeight}px`;
chartDom.offsetHeight; // 强制重排
},
// 检查是否需要重试
shouldRetry() {
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
const availableHeight = viewportHeight - 120;
const availableWidth = viewportWidth - 80;
return (availableWidth <= 0 || availableHeight <= 0) && this.initRetryCount < this.maxRetryCount;
},
// 安排重试
scheduleRetry() {
this.initRetryCount++;
setTimeout(() => this.initChart(), 500);
},
// 重置重试计数
resetRetryCount() {
this.initRetryCount = 0;
},
// 销毁图表
disposeChart() {
if (this.myChart) {
this.myChart.dispose();
}
},
// 创建图表实例
createChart(chartDom) {
this.myChart = echarts.init(chartDom, null, {
renderer: 'canvas',
useDirtyRect: true
});
},
// 检查数据有效性
hasValidData(mockData) {
return mockData &&
mockData.wellData &&
mockData.wellData.depthLine &&
mockData.wellData.depthLine.length > 0;
},
// 设置事件监听器
setupEventListeners() {
window.addEventListener("resize", this.handleResize);
this.observeParentResize();
},
// 清理资源
cleanup() {
window.removeEventListener("resize", this.handleResize);
if (this.myChart) {
this.myChart.dispose();
}
if (this.resizeObserver) {
this.resizeObserver.disconnect();
}
if (this.debounceTimer) {
clearTimeout(this.debounceTimer);
}
},
// 观察父容器大小变化
observeParentResize() {
const parentDom = this.$refs.chartRef?.parentElement;
if (parentDom && window.ResizeObserver) {
this.resizeObserver = new ResizeObserver(() => {
this.debouncedResize();
});
this.resizeObserver.observe(parentDom);
}
},
// 获取数据
async getList() {
try {
const res = await getdjZft({
jhs: `${this.jh},${this.jhs}`
});
this.mockData = res?.mockData || {};
const legendList = Array.isArray(res?.tlList)
? res.tlList
: Array.isArray(res?.mockData?.tlList)
? res.mockData.tlList
: [];
this.legendItems = legendList;
return this.mockData;
} catch (error) {
console.error("获取数据失败:", error);
throw error;
}
},
// 获取当前主井的录井多曲线数据(钻时等),用于在同一张图上叠加折线
async getCurveData() {
// 优先用 props.jh;如果没有,再从 jhs 里取第一个井号做回退
let targetJh = this.jh;
if (!targetJh && this.jhs) {
const arr = String(this.jhs)
.split(",")
.map((s) => s.trim())
.filter((s) => s);
if (arr.length) {
targetJh = arr[0];
}
}
console.log("HistogramGraph.getCurveData => jh:", targetJh, "props:", {
jh: this.jh,
jhs: this.jhs,
});
if (!targetJh) {
console.warn("HistogramGraph.getCurveData: 无有效井号,跳过获取曲线数据");
return null;
}
try {
const [ljqxRes, ljSssjRes] = await Promise.all([
getljqxData({ jh: targetJh }),
listLjSssjSd({ jh: targetJh, pageNum: 1, pageSize: 999999 }),
]);
console.log("HistogramGraph.getCurveData 接口返回:", {
ljqxRes,
ljSssjRes,
});
const processed = this.processCurveData(ljqxRes, ljSssjRes);
console.log("HistogramGraph.getCurveData 处理后曲线数据:", processed);
return processed;
} catch (e) {
console.error("获取录井多曲线数据失败:", e);
return null;
}
},
// 处理录井多曲线数据,结构参考 DrillingTimeChart.vue
processCurveData(ljqxRes, ljSssjRes) {
console.log("HistogramGraph.processCurveData 输入原始数据:", {
ljqxRes,
ljSssjRes,
});
const processArrayData = (dataList, fieldName) => {
if (!dataList || !Array.isArray(dataList) || dataList.length === 0) {
return [];
}
// 对象数组 [{dept: xxx, value: xxx}, ...]
if (typeof dataList[0] === "object" && dataList[0].hasOwnProperty("dept")) {
return dataList.map((item) => ({
depth: item.dept || item.depth,
value: item[fieldName] || item.value || item,
}));
}
// 普通数组,深度从录井整米数据里取
return dataList.map((value, index) => {
const depth =
ljSssjRes && ljSssjRes.rows && ljSssjRes.rows[index]
? ljSssjRes.rows[index].js
: null;
return { depth, value };
});
};
// 钻时:录井整米数据 rows.zs
let drillingTimeData = [];
if (ljSssjRes && ljSssjRes.rows && Array.isArray(ljSssjRes.rows)) {
drillingTimeData = ljSssjRes.rows.map((item) => ({
depth: item.js,
value: item.zs,
}));
}
const torqueData = processArrayData(ljqxRes.njList, "nj");
const standpipePressureData = processArrayData(ljqxRes.lyList, "ly");
const drillingPressureData = processArrayData(ljqxRes.zyList, "zy");
const rpmData = processArrayData(ljqxRes.zs1List, "zs1");
const rkllData = processArrayData(ljqxRes.rkllList, "rkll");
// 泵冲:录井整米数据 rows.zbc
let pumpStrokeData = [];
if (ljSssjRes && ljSssjRes.rows && Array.isArray(ljSssjRes.rows)) {
pumpStrokeData = ljSssjRes.rows.map((item) => ({
depth: item.js,
value: item.zbc,
}));
}
const result = {
drillingTime: drillingTimeData,
torque: torqueData,
standpipePressure: standpipePressureData,
drillingPressure: drillingPressureData,
rpm: rpmData,
pumpStroke: pumpStrokeData,
rkllData: rkllData,
};
console.log("HistogramGraph.processCurveData 输出:", {
drillingTimeLen: drillingTimeData.length,
torqueLen: torqueData.length,
standpipePressureLen: standpipePressureData.length,
drillingPressureLen: drillingPressureData.length,
rpmLen: rpmData.length,
pumpStrokeLen: pumpStrokeData.length,
rkllLen: rkllData.length,
result,
});
return result;
},
// 供父组件在切换到本tab时调用
async loadData() {
await this.initChart();
},
// 处理窗口大小变化
handleResize() {
this.debouncedResize();
},
// 防抖处理resize
debouncedResize() {
if (this.debounceTimer) {
clearTimeout(this.debounceTimer);
}
this.debounceTimer = setTimeout(() => {
this.performResize();
}, 200);
},
// 执行resize操作
performResize() {
if (this.myChart) {
const chartDom = this.$refs.chartRef;
if (chartDom) {
this.setChartDimensions(chartDom);
}
this.$nextTick(() => {
if (this.myChart) {
this.myChart.resize();
if (this.lastStackedAreas && this.lastChartConfig && this.lastXAxisLabels) {
this.drawStratumLabels(this.lastStackedAreas, this.lastChartConfig, this.lastXAxisLabels);
}
if (this.lastDepthIntervals && this.lastXAxisLabels && this.lastChartConfig) {
this.drawJhSeparators(this.lastDepthIntervals, this.lastXAxisLabels, this.lastChartConfig);
}
if (this.lastXAxisLabels && this.lastChartConfig) {
this.drawCategoryEdgeLines(this.lastXAxisLabels, this.lastChartConfig);
this.drawTopBandSvg(this.lastXAxisLabels, this.lastChartConfig, this.lastStackedAreas);
}
}
});
}
},
// 刷新图表
async refreshChart() {
if (this.myChart) {
await this.initChart();
}
},
// 渲染真实数据
async renderRealData(mockData) {
try {
const {wellData} = mockData;
console.log(wellData, 'wellData');
if (this.curveData) {
console.log("HistogramGraph.renderRealData 当前曲线数据:", this.curveData);
} else {
console.warn("HistogramGraph.renderRealData: 当前 curveData 为空或未获取到");
}
const xAxisLabels = wellData.depthLine.map((point) => point.x);
try {
window.__hist_xLabels = xAxisLabels;
} catch (e) {
}
const option = {
...this.getDefaultChartOption(),
xAxis: this.createXAxis(xAxisLabels),
yAxis: this.createYAxis(mockData.chartConfig),
series: this.createSeries(wellData)
};
// 如果有录井多曲线数据,则在同一 grid 中叠加一组使用 value 型 x 轴的折线
if (this.curveData) {
this.extendWithCurves(option, mockData.chartConfig);
}
const chartDom = this.$refs.chartRef;
if (chartDom) {
this.setChartDimensions(chartDom);
}
this.myChart.setOption(option, true);
// 确保图表完全渲染后再resize和绘制
this.$nextTick(() => {
// 先resize确保图表尺寸正确
this.myChart.resize();
// 使用 requestAnimationFrame 确保浏览器完成渲染后再绘制
requestAnimationFrame(() => {
requestAnimationFrame(() => {
// 再次确认尺寸,确保图表已完全渲染
const chartDom = this.$refs.chartRef;
if (chartDom && chartDom.offsetWidth > 0 && chartDom.offsetHeight > 0) {
// 再次resize确保尺寸准确
this.myChart.resize();
// 保存数据用于后续resize时重新绘制
this.lastStackedAreas = wellData.stackedAreas;
this.lastChartConfig = mockData.chartConfig;
this.lastXAxisLabels = xAxisLabels;
this.lastDepthIntervals = wellData.depthIntervals || [];
// 绘制所有图形元素
this.drawStratumLabels(this.lastStackedAreas, this.lastChartConfig, this.lastXAxisLabels);
this.drawJhSeparators(this.lastDepthIntervals, this.lastXAxisLabels, this.lastChartConfig);
this.drawCategoryEdgeLines(this.lastXAxisLabels, this.lastChartConfig);
this.drawTopBandSvg(this.lastXAxisLabels, this.lastChartConfig, this.lastStackedAreas);
} else {
// 如果尺寸还未确定,延迟重试
setTimeout(() => {
if (this.myChart) {
this.myChart.resize();
this.lastStackedAreas = wellData.stackedAreas;
this.lastChartConfig = mockData.chartConfig;
this.lastXAxisLabels = xAxisLabels;
this.lastDepthIntervals = wellData.depthIntervals || [];
this.drawStratumLabels(this.lastStackedAreas, this.lastChartConfig, this.lastXAxisLabels);
this.drawJhSeparators(this.lastDepthIntervals, this.lastXAxisLabels, this.lastChartConfig);
this.drawCategoryEdgeLines(this.lastXAxisLabels, this.lastChartConfig);
this.drawTopBandSvg(this.lastXAxisLabels, this.lastChartConfig, this.lastStackedAreas);
}
}, 150);
}
});
});
});
} catch (error) {
console.error("渲染数据失败:", error);
this.handleError(error);
}
},
// 获取默认图表配置
getDefaultChartOption() {
const colors = this.colorScheme;
return {
title: {text: "", subtext: ""},
aria: {enabled: false},
backgroundColor: colors.background,
tooltip: {
trigger: "axis",
axisPointer: {
type: "cross",
label: {backgroundColor: colors.primary, show:false},
crossStyle: {color: colors.border}
},
backgroundColor: 'rgba(255,255,255,0.95)',
borderColor: colors.border,
borderWidth: 1,
textStyle: {color: colors.text},
extraCssText: 'box-shadow: 0 4px 12px rgba(0,0,0,0.15); border-radius: 8px;',
formatter: (params) => {
if (!params || params.length === 0) return '';
const dataIndex = params[0].dataIndex;
const depthInterval = this.mockData?.wellData?.depthIntervals?.[dataIndex];
if (!depthInterval) {
// 如果没有depthInterval数据,使用默认格式
let result = `<div style="font-weight: 600; margin-bottom: 8px; color: ${colors.primary};">${params[0].axisValue}</div>`;
params.forEach(param => {
const color = param.color;
result += `<div style="margin: 4px 0;">
<span style="display: inline-block; width: 10px; height: 10px; background-color: ${color}; border-radius: 50%; margin-right: 8px;"></span>
<span style="font-weight: 500;">${param.seriesName}:</span>
<span style="margin-left: 8px; font-weight: 600;">${param.value}</span>
</div>`;
});
return result;
}
// 显示井号信息
let result = `<div style="font-weight: 600; margin-bottom: 8px; color: ${colors.primary};">${depthInterval.jh || depthInterval.x}</div>`;
// 显示详细信息
const modelLabel = this.formatXAxisLabel(depthInterval.x || '-');
result += `<div style="margin: 4px 0; padding: 4px 0; border-top: 1px solid ${colors.border};">
<div style="margin: 2px 0;"><span style="font-weight: 500;">型号:</span> <span style="margin-left: 8px; font-weight: 600;">${modelLabel}</span></div>
<div style="margin: 2px 0;"><span style="font-weight: 500;">尺寸:</span> <span style="margin-left: 8px; font-weight: 600;">${depthInterval.ztcc || '-'}</span></div>
<div style="margin: 2px 0;"><span style="font-weight: 500;">进尺mm:</span> <span style="margin-left: 8px; font-weight: 600;">${depthInterval.jc || '-'}</span></div>
<div style="margin: 2px 0;"><span style="font-weight: 500;">深度区间:</span> <span style="margin-left: 8px; font-weight: 600;">${depthInterval.interval || '-'}</span></div>
</div>`;
// 只显示非地层信息的系列数据(过滤掉地层相关的系列)
// params.forEach(param => {
// // 过滤掉地层相关的系列名称
// if (param.seriesName && !param.seriesName.includes('地层') && param.seriesName !== '占位') {
// const color = param.color;
// result += `<div style="margin: 4px 0;">
// <span style="display: inline-block; width: 10px; height: 10px; background-color: ${color}; border-radius: 50%; margin-right: 8px;"></span>
// <span style="font-weight: 500;">${param.seriesName}:</span>
// <span style="margin-left: 8px; font-weight: 600;">${param.value}</span>
// </div>`;
// }
// });
return result;
}
},
grid: {
top: 20,
left: "2%",
right: "3%",
bottom: "10%",
containLabel: true,
show: false,
},
animation: true,
animationDuration: 1500,
animationEasing: 'cubicOut',
animationDelay: function (idx) {
return idx * 100;
}
};
},
// 处理区域系列
async processAreaSeries(wellData, xAxisLabels) {
const stackedAreas = wellData?.stackedAreas || [];
// 兼容旧格式:数组(所有井已按全局顺序平铺)
if (Array.isArray(stackedAreas)) {
return await Promise.all(
stackedAreas
.filter(area => area.svg !== null)
.map(async (area) => {
let areaStyle = {opacity: 0.6};
if (area.svg) {
try {
const svgImage = await this.createSvgImage(area.svg);
areaStyle.color = {image: svgImage, repeat: "repeat"};
} catch (error) {
console.error('SVG转换失败:', error);
}
}
return {
name: area.name,
type: "line",
xAxisIndex: 1,
z: -1,
stack: "总量",
symbol: "none",
showSymbol: false,
areaStyle: areaStyle,
lineStyle: {width: 0},
data: area.points.map((point) => (point && point.y2 != null) ? point.y2 : point.y),
};
})
);
}
// 新格式:对象,key 为井号,每口井内是该井的地层数组
const totalLen = Array.isArray(xAxisLabels) ? xAxisLabels.length : 0;
console.log(totalLen, 'totalLen');
// 全局 x 标签 -> 索引数组 映射
const xToIndices = new Map();
console.log(xToIndices, 'xToIndices');
(xAxisLabels || []).forEach((x, idx) => {
const key = String(x);
if (!xToIndices.has(key)) xToIndices.set(key, []);
xToIndices.get(key).push(idx);
});
const buildNullArray = (len) => Array.from({length: len}, () => null);
console.log(buildNullArray, 'buildNullArray');
const results = [];
for (const [jh, layers] of Object.entries(stackedAreas)) {
if (!Array.isArray(layers) || layers.length === 0) continue;
// 估算该井的横向覆盖范围:取该井所有层的所有点对应的全局索引的 min/max
const indicesForJh = [];
const missingInXAxis = [];
for (const area of layers) {
for (const p of (area?.points || [])) {
const arr = xToIndices.get(String(p?.x)) || [];
if (arr.length === 0) {
missingInXAxis.push({x: p?.x, jh, areaName: area?.name});
} else {
indicesForJh.push(...arr);
}
}
}
const startIdx = Math.min(...indicesForJh);
const endIdx = Math.max(...indicesForJh);
for (const area of (layers || [])) {
if (area == null) continue;
let areaStyle = {opacity: 0.6};
if (area.svg) {
try {
const svgImage = await this.createSvgImage(area.svg);
areaStyle.color = {image: svgImage, repeat: "repeat"};
} catch (error) {
console.error('SVG转换失败:', error);
}
}
const data = buildNullArray(totalLen);
// 把该层每个点的 y2/y 填到该点 x 对应的全局索引中,仅限落在该井的覆盖范围内
const outOfRangePoints = [];
for (const p of (area.points || [])) {
const yVal = (p && p.y2 != null) ? p.y2 : p?.y;
const cands = xToIndices.get(String(p?.x)) || [];
if (cands.length === 0) continue;
let placed = false;
cands.forEach(ix => {
if (ix >= startIdx && ix <= endIdx) {
data[ix] = yVal;
placed = true;
}
});
if (!placed) {
outOfRangePoints.push({x: p?.x, cands, jh, areaName: area?.name, startIdx, endIdx});
}
}
results.push({
name: area.name,
type: "line",
xAxisIndex: 1,
z: -1,
stack: "总量",
symbol: "none",
showSymbol: false,
areaStyle: areaStyle,
lineStyle: {width: 0},
data
});
// 仅在有异常时输出调试信息,方便定位 x 匹配问题
if (missingInXAxis.length > 0 || outOfRangePoints.length > 0) {
// 使用 console.group 让日志更清晰
console.groupCollapsed && console.groupCollapsed(`层定位告警: 井号=${jh}`);
if (missingInXAxis.length > 0) {
console.warn('x 不在 xAxisLabels 中:', missingInXAxis);
}
if (outOfRangePoints.length > 0) {
console.warn('x 命中但不在井段范围内:', outOfRangePoints);
console.info('井段范围:', {jh, startIdx, endIdx});
}
console.groupEnd && console.groupEnd();
}
}
}
return results;
},
// 创建X轴配置
createXAxis(xAxisLabels) {
const colors = this.colorScheme;
const formatLabel = (value) => this.formatXAxisLabel(value);
return [
{
type: "category",
boundaryGap: true,
position: "top",
data: xAxisLabels,
axisLabel: {
interval: 0,
rotate: -90,
margin: 30,
align: "center",
fontSize: 12,
color: colors.text,
fontWeight: 500,
formatter: formatLabel
},
axisTick: {
alignWithLabel: true,
length: 6,
lineStyle: {color: colors.border}
},
splitLine: {show: false},
axisLine: {
show: true,
lineStyle: {
color: colors.border,
width: 2
}
}
},
{
type: "category",
boundaryGap: false,
position: "top",
show: false,
data: xAxisLabels
},
];
},
// 创建Y轴配置
createYAxis(chartConfig) {
const colors = this.colorScheme;
return [
{
type: "value",
name: "井深(m)",
nameTextStyle: {
color: colors.text,
fontSize: 13,
fontWeight: 600,
padding: [0, 0, 0, 8]
},
min: chartConfig.yAxis.min,
max: chartConfig.yAxis.max,
interval: chartConfig.yAxis.interval,
inverse: true,
axisLabel: {
formatter: "{value}",
fontSize: 12,
color: colors.text,
fontWeight: 500
},
splitLine: {
show: true,
lineStyle: {
color: colors.border,
type: 'dashed',
opacity: 0.3
}
},
axisLine: {
show: true,
lineStyle: {
color: colors.border,
width: 2
}
},
axisTick: {
show: true,
lineStyle: {color: colors.border}
}
},
{
type: "value",
name: "深度(m)",
nameTextStyle: {
color: colors.text,
fontSize: 13,
fontWeight: 600,
padding: [0, 8, 0, 55]
},
min: chartConfig.yAxis2.min,
max: chartConfig.yAxis2.max,
interval: chartConfig.yAxis2.interval,
position: "right",
axisLabel: {
formatter: "{value}",
fontSize: 12,
color: colors.text,
fontWeight: 500
},
splitLine: {show: false},
axisLine: {
show: true,
lineStyle: {
color: colors.border,
width: 2
}
},
axisTick: {
show: true,
lineStyle: {color: colors.border}
}
},
];
},
// 创建系列配置
createSeries(wellData, areaSeries) {
const colors = this.colorScheme;
return [
{
name: "井深数据",
type: "line",
zlevel: 25,
yAxisIndex: 1,
symbol: "circle",
symbolSize: 10,
itemStyle: {
color: colors.accent,
borderColor: '#fff',
borderWidth: 3,
shadowColor: 'rgba(0,0,0,0.2)',
shadowBlur: 8
},
lineStyle: {
color: colors.accent,
width: 3,
type: 'solid',
shadowColor: 'rgba(0,0,0,0.1)',
shadowBlur: 4
},
label: {
show: true,
position: "top",
formatter: "{c}",
fontSize: 13,
fontWeight: 600,
color: colors.accent,
backgroundColor: "rgba(255,255,255,0.95)",
padding: [6, 8],
borderRadius: 6,
borderColor: colors.accent,
borderWidth: 2,
shadowColor: 'rgba(0,0,0,0.1)',
shadowBlur: 4
},
data: wellData.depthLine.map((point) => point.y),
},
{
name: "占位",
type: "bar",
zlevel: 20,
stack: "total",
silent: true,
barWidth: "20%",
itemStyle: {
borderColor: "transparent",
color: "transparent"
},
emphasis: {
itemStyle: {
borderColor: "transparent",
color: "transparent"
}
},
data: wellData.depthIntervals.map((item) => item.placeholder),
},
{
name: "深度区间",
type: "bar",
zlevel: 20,
stack: "total",
barWidth: "15%",
label: {
show: true,
position: 'insideTop',
color: '#fff',
fontSize: 11,
fontWeight: 600,
backgroundColor: 'rgba(0,0,0,0.1)',
padding: [2, 3],
borderRadius: 4,
borderColor: 'rgba(255,255,255,0.3)',
borderWidth: 1,
formatter: (params) => {
const di = wellData.depthIntervals[params.dataIndex];
const value = di?.jc ?? di?.interval;
if (value === undefined || value === null || value === '') return params.data;
return `${value}`;
}
},
itemStyle: {
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{offset: 0, color: colors.gradient.start},
{offset: 1, color: colors.gradient.end}
]
},
borderRadius: [4, 4, 0, 0],
shadowColor: 'rgba(0,0,0,0.2)',
shadowBlur: 8,
shadowOffsetY: 2
},
emphasis: {
itemStyle: {
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{offset: 0, color: this.adjustColor(colors.gradient.start, 1.2)},
{offset: 1, color: this.adjustColor(colors.gradient.end, 1.2)}
]
},
shadowBlur: 12,
shadowOffsetY: 4
}
},
data: wellData.depthIntervals.map((item) => {
const value = item?.jc ?? item?.interval;
return typeof value === 'number' ? value : Number(value) || 0;
}),
},
// 底部显示 interval 数值的透明条,用于放置底部标签
{
name: '深度区间-底部标签',
type: 'bar',
zlevel: 20,
// 与主柱重叠以便把标签放在柱底部区域内
barGap: '-100%',
barWidth: '20%',
itemStyle: {
color: 'transparent',
borderColor: 'transparent'
},
emphasis: {itemStyle: {color: 'transparent', borderColor: 'transparent'}},
label: {
show: false,
position: 'insideBottom',
color: '#fff',
fontSize: 11,
fontWeight: 600,
backgroundColor: 'rgba(0,0,0,0.1)',
padding: [2, 3],
borderRadius: 4,
borderColor: 'rgba(255,255,255,0.3)',
borderWidth: 1,
distance: 0,
formatter: (params) => {
const di = wellData.depthIntervals[params.dataIndex];
const value = di?.jc ?? di?.interval;
if (value === undefined || value === null || value === '') return params.data;
return `进尺 ${value}`;
}
},
data: wellData.depthIntervals.map((item) => {
const value = item?.jc ?? item?.interval;
return typeof value === 'number' ? value : Number(value) || 0;
})
}
];
},
// 在现有直方图配置上叠加 6 条曲线:统一 y 轴为深度,新建一条 value 型 x 轴承载数值
extendWithCurves(option, chartConfig) {
const cd = this.curveData;
if (!cd) {
console.warn("HistogramGraph.extendWithCurves: 无 curveData,直接返回");
return;
}
console.log("HistogramGraph.extendWithCurves 收到 curveData:", cd);
// 收集所有数值,确定新 x 轴范围
const collectValues = (arr) =>
(arr || [])
.map((p) => (p && p.value != null ? Number(p.value) : null))
.filter((v) => v != null && !Number.isNaN(v));
const collectDepths = (arr) =>
(arr || [])
.map((p) => (p && p.depth != null ? Number(p.depth) : null))
.filter((v) => v != null && !Number.isNaN(v));
const allVals = [
...collectValues(cd.drillingTime),
...collectValues(cd.torque),
...collectValues(cd.standpipePressure),
...collectValues(cd.drillingPressure),
...collectValues(cd.rpm),
...collectValues(cd.pumpStroke),
...collectValues(cd.rkllData),
];
const allDepths = [
...collectDepths(cd.drillingTime),
...collectDepths(cd.torque),
...collectDepths(cd.standpipePressure),
...collectDepths(cd.drillingPressure),
...collectDepths(cd.rpm),
...collectDepths(cd.pumpStroke),
...collectDepths(cd.rkllData),
];
console.log(
"HistogramGraph.extendWithCurves 所有数值长度:",
allVals.length,
"示例前若干值:",
allVals.slice(0, 20)
);
if (!allVals.length) {
console.warn("HistogramGraph.extendWithCurves: 所有曲线数值为空,不生成折线");
return;
}
if (allDepths.length) {
const minDepthCurve = Math.min(...allDepths);
const maxDepthCurve = Math.max(...allDepths);
console.log("HistogramGraph.extendWithCurves 曲线深度范围:", {
minDepthCurve,
maxDepthCurve,
yAxis0Before: option.yAxis && option.yAxis[0],
});
// 尝试扩展左侧深度轴范围,确保曲线不会被裁剪在 y 轴之外
if (Array.isArray(option.yAxis) && option.yAxis[0]) {
const y0 = option.yAxis[0];
if (typeof y0.min === "number" && typeof y0.max === "number") {
y0.min = Math.min(y0.min, minDepthCurve);
y0.max = Math.max(y0.max, maxDepthCurve);
}
}
}
const minVal = Math.min(...allVals);
const maxVal = Math.max(...allVals);
const span = maxVal - minVal || 1;
const padding = span * 0.1;
const axisMin = minVal - padding;
const axisMax = maxVal + padding;
console.log("HistogramGraph.extendWithCurves x 轴范围:", {
minVal,
maxVal,
axisMin,
axisMax,
});
// 新增一条 value 型 x 轴,放在底部,专门给多曲线使用
const xAxisIndex = Array.isArray(option.xAxis) ? option.xAxis.length : 0;
if (!Array.isArray(option.xAxis)) option.xAxis = [];
option.xAxis.push({
type: "value",
position: "bottom",
name: "钻时 / 扭矩 / 立压 / 钻压 / 转速 / 泵冲 / 入口流量",
nameLocation: "center",
nameGap: 45,
min: axisMin,
max: axisMax,
axisLine: {
show: true,
lineStyle: {
color: "#9ca3af",
width: 1.5,
},
},
axisLabel: {
fontSize: 11,
color: "#4b5563",
},
splitLine: {
show: false,
},
});
if (!Array.isArray(option.series)) option.series = [];
const makeSeriesData = (arr) =>
(arr || [])
.filter(
(p) =>
p &&
p.depth != null &&
p.value != null &&
!Number.isNaN(Number(p.value))
)
.map((p) => [Number(p.value), p.depth])
.sort((a, b) => a[1] - b[1]); // 按深度排序
const pushCurve = (name, arr, color, yAxisIndex, lineWidth = 1) => {
const data = makeSeriesData(arr);
if (!data.length) return;
option.series.push({
name,
type: "line",
xAxisIndex,
yAxisIndex,
// 提高 zlevel,确保在柱子和地层之上渲染
zlevel: 30,
z: 30,
symbol: "none",
smooth: true,
lineStyle: {
color,
width: lineWidth,
},
data,
});
};
// 统一使用左侧深度轴(index 0),保证纵向坐标语义一致
pushCurve("钻时 (min/m)", cd.drillingTime, "#FF0000", 0, 2);
pushCurve("扭矩 (kN•m)", cd.torque, "#1E90FF", 0);
pushCurve("立压 (MPa)", cd.standpipePressure, "#32CD32", 0);
pushCurve("钻压 (kN)", cd.drillingPressure, "#FFD700", 0);
pushCurve("转速 (rpm)", cd.rpm, "#FF6347", 0);
pushCurve("泵冲", cd.pumpStroke, "#9370DB", 0);
pushCurve("入口流量", cd.rkllData, "#20B2AA", 0);
},
// 颜色调整工具方法
adjustColor(color, factor) {
if (color.startsWith('#')) {
const r = parseInt(color.slice(1, 3), 16);
const g = parseInt(color.slice(3, 5), 16);
const b = parseInt(color.slice(5, 7), 16);
const newR = Math.min(255, Math.round(r * factor));
const newG = Math.min(255, Math.round(g * factor));
const newB = Math.min(255, Math.round(b * factor));
return `rgb(${newR}, ${newG}, ${newB})`;
}
return color;
},
// 渲染空状态
renderEmpty(chartDom) {
const colors = this.colorScheme;
chartDom.innerHTML = `
<div class="empty-state" style="
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: ${colors.text};
font-size: 16px;
background: ${colors.background};
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
">
<div style="
font-size: 48px;
margin-bottom: 16px;
opacity: 0.6;
color: ${colors.primary};
">📊</div>
<div style="
font-size: 18px;
color: ${colors.text};
font-weight: 500;
">暂无数据</div>
<div style="
font-size: 14px;
color: ${colors.text};
opacity: 0.6;
margin-top: 8px;
">请检查数据配置</div>
</div>
`;
},
// 处理错误
handleError(error) {
console.error("图表错误:", error);
},
formatXAxisLabel(value) {
if (value == null) return "";
const str = String(value);
const idx = str.indexOf("-");
return idx === -1 ? str : str.slice(0, idx);
},
// 创建SVG图片
createSvgImage(svgString) {
return new Promise((resolve, reject) => {
const svgBlob = new Blob([svgString], {type: 'image/svg+xml;charset=utf-8'});
const svgUrl = URL.createObjectURL(svgBlob);
const img = new Image();
img.onload = () => {
URL.revokeObjectURL(svgUrl);
resolve(img);
};
img.onerror = (e) => {
URL.revokeObjectURL(svgUrl);
reject(e);
};
img.src = svgUrl;
});
},
// 为右侧图例生成纹理样式
getLegendSwatchStyle(item = {}) {
const baseStyle = {
backgroundColor: '#d1d5db',
backgroundRepeat: 'repeat',
backgroundSize: '36px 36px'
};
if (!item?.svg) return baseStyle;
const dataUrl = this.getSvgDataUrl(item.svg);
if (!dataUrl) return baseStyle;
return {
backgroundImage: `url("${dataUrl}")`,
backgroundRepeat: 'repeat',
backgroundSize: '36px 36px',
backgroundColor: 'transparent'
};
},
getSvgDataUrl(svgString) {
if (!svgString || typeof svgString !== 'string') return '';
try {
const compact = svgString.replace(/\s+/g, ' ').trim();
return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(compact)}`;
} catch (e) {
console.warn('SVG 编码失败:', e);
return '';
}
},
// 层位名称不再图内显示,仅清理旧的文字元素
drawStratumLabels() {
if (!this.myChart) return;
const elements = Array.isArray(this.currentGraphicElements)
? this.currentGraphicElements.filter(el => !el.__stratumLabel)
: [];
this.currentGraphicElements = elements;
this.$nextTick(() => {
if (this.myChart) {
this.myChart.setOption({graphic: {elements}}, {replaceMerge: ['graphic']});
}
});
},
// 根据 depthIntervals 的顺序按 jh 分段,绘制虚线与顶部井号
drawJhSeparators(depthIntervals, xAxisLabels, chartConfig) {
if (!this.myChart || !depthIntervals || depthIntervals.length === 0) return;
// 扫描 depthIntervals 顺序,形成段
const segments = [];
let currentJh = null;
let currentMin = Infinity;
let currentMax = -Infinity;
let started = false;
for (let i = 0; i < depthIntervals.length; i++) {
// 直接使用在 depthIntervals 中的顺序索引作为 x 轴位置,
// 以避免相同 x 标签造成的覆盖与错位
const {jh} = depthIntervals[i];
const idx = i;
if (!started) {
currentJh = jh;
currentMin = currentMax = idx;
started = true;
continue;
}
if (jh === currentJh) {
currentMin = Math.min(currentMin, idx);
currentMax = Math.max(currentMax, idx);
} else {
segments.push({
jh: currentJh,
startIdx: Math.min(currentMin, currentMax),
endIdx: Math.max(currentMin, currentMax)
});
// 开始新段
currentJh = jh;
currentMin = currentMax = idx;
}
}
if (started) {
segments.push({
jh: currentJh,
startIdx: Math.min(currentMin, currentMax),
endIdx: Math.max(currentMin, currentMax)
});
}
const graphics = [];
// 确保图表已完全渲染,检查容器尺寸
const chartDom = this.$refs.chartRef;
if (!chartDom || chartDom.offsetWidth === 0 || chartDom.offsetHeight === 0) {
console.warn('图表容器尺寸无效,跳过绘制井号分隔符');
return;
}
// 垂直范围(像素)——考虑 y 轴 inverse,取像素最小为顶部、最大为底部
const yMinPx = this.myChart.convertToPixel({yAxisIndex: 0}, chartConfig.yAxis.min);
const yMaxPx = this.myChart.convertToPixel({yAxisIndex: 0}, chartConfig.yAxis.max);
if (yMinPx === null || yMaxPx === null || isNaN(yMinPx) || isNaN(yMaxPx)) {
console.warn('convertToPixel 返回无效值,跳过绘制井号分隔符');
return;
}
const yTopPx = Math.min(yMinPx, yMaxPx);
const yBottomPx = Math.max(yMinPx, yMaxPx);
// 边界虚线(段与段之间),改为更浅的灰色虚线
for (let i = 0; i < segments.length - 1; i++) {
const leftEnd = segments[i].endIdx;
const rightStart = segments[i + 1].startIdx;
const pxLeft = this.myChart.convertToPixel({xAxisIndex: 0}, leftEnd);
const pxRight = this.myChart.convertToPixel({xAxisIndex: 0}, rightStart);
if (pxLeft === null || pxRight === null) {
console.warn('convertToPixel 返回 null 值,跳过绘制边界虚线');
continue;
}
const midX = (pxLeft + pxRight) / 2;
graphics.push({
type: 'line',
shape: {x1: midX, y1: yTopPx, x2: midX, y2: yBottomPx},
style: {stroke: '#bbb', lineDash: [8, 6], lineWidth: 1.5},
silent: true,
z: 100,
zlevel: 10
});
}
// 每段顶部显示 jh(居中),带背景标签并做简单防重叠处理
let lastLabelRight = -Infinity;
let rowShift = 0; // 逐行上移避免覆盖
segments.forEach((seg, idx) => {
if (!seg.jh) return;
const pxStart = this.myChart.convertToPixel({xAxisIndex: 0}, seg.startIdx);
const pxEnd = this.myChart.convertToPixel({xAxisIndex: 0}, seg.endIdx);
if (pxStart === null || pxEnd === null) {
console.warn('convertToPixel 返回 null 值,跳过绘制井号标签');
return;
}
const midX = (pxStart + pxEnd) / 2;
let topY = yTopPx - 80; // 再上移,提升显示位置
// 估算标签宽度用于避免与上一个重叠
const fontSize = 12;
const paddingH = 6;
const estimatedWidth = seg.jh.length * fontSize * 0.6 + paddingH * 2 + 10;
const labelLeft = midX - estimatedWidth / 2;
const labelRight = midX + estimatedWidth / 2;
if (labelLeft < lastLabelRight) {
rowShift += 18; // 叠一行
} else {
rowShift = 0;
}
lastLabelRight = Math.max(lastLabelRight, labelRight);
topY -= rowShift;
const labelWidth = Math.max(estimatedWidth, 80);
const labelHeight = 22;
const pointerSize = 6;
graphics.push({
type: 'group',
x: midX - labelWidth / 2,
y: topY - labelHeight,
z: 101,
zlevel: 10,
silent: true,
children: [
{
type: 'rect',
shape: {x: 0, y: 0, width: labelWidth, height: labelHeight, r: 8},
style: {
fill: '#2563eb',
stroke: '#1e40af',
lineWidth: 1.5,
shadowBlur: 6,
shadowColor: 'rgba(37,99,235,0.35)'
}
},
{
type: 'polygon',
shape: {
points: [
[labelWidth / 2 - pointerSize, labelHeight],
[labelWidth / 2 + pointerSize, labelHeight],
[labelWidth / 2, labelHeight + pointerSize]
]
},
style: {fill: '#2563eb', stroke: '#1e40af'}
},
{
type: 'text',
style: {
x: labelWidth / 2,
y: labelHeight / 2,
text: seg.jh,
fill: '#fff',
fontSize: fontSize + 1,
fontWeight: 700,
align: 'center',
verticalAlign: 'middle'
},
zlevel: 40
}
]
});
});
// 叠加到现有 graphic 上(保留原有 graphic)
const prevElements = Array.isArray(this.currentGraphicElements) ? this.currentGraphicElements : [];
const merged = prevElements.concat(graphics);
this.currentGraphicElements = merged;
this.$nextTick(() => {
if (this.myChart) {
this.myChart.setOption({graphic: {elements: merged}}, {replaceMerge: ['graphic']});
}
});
},
// 为每个 x 类目在左右各画一条黑色竖线(仅顶部短竖线)
drawCategoryEdgeLines(xAxisLabels, chartConfig) {
if (!this.myChart || !Array.isArray(xAxisLabels) || xAxisLabels.length === 0) return;
// 确保图表已完全渲染,检查容器尺寸
const chartDom = this.$refs.chartRef;
if (!chartDom || chartDom.offsetWidth === 0 || chartDom.offsetHeight === 0) {
console.warn('图表容器尺寸无效,跳过绘制类目边界线');
return;
}
const yMinPx = this.myChart.convertToPixel({yAxisIndex: 0}, chartConfig.yAxis.min);
const yMaxPx = this.myChart.convertToPixel({yAxisIndex: 0}, chartConfig.yAxis.max);
if (yMinPx === null || yMaxPx === null || isNaN(yMinPx) || isNaN(yMaxPx)) {
console.warn('convertToPixel 返回无效值,跳过绘制类目边界线');
return;
}
const yTopPx = Math.min(yMinPx, yMaxPx);
const yBottomPx = Math.max(yMinPx, yMaxPx);
// 只在上方画短竖线:长度占绘图区高度的 6%,并限制在 15-60 像素
const plotHeight = Math.abs(yBottomPx - yTopPx);
const stemLen = Math.max(15, Math.min(60, plotHeight * 0.06));
// 仅保留上方的线:整段都在 top 之上
const extraHead = Math.max(6, Math.min(30, plotHeight * 0.03));
const yStemStart = yTopPx - (extraHead + stemLen);
const yStemEnd = yTopPx - 1;
const graphics = [];
const n = xAxisLabels.length;
// 计算每个类目的像素中心
const centers = [];
for (let i = 0; i < n; i++) {
const cx = this.myChart.convertToPixel({xAxisIndex: 0}, i);
if (cx != null) centers.push(cx);
}
if (centers.length > 0) {
// 第一个点左侧边界:用前两个中心的半步估计
let firstEdge;
if (centers.length >= 2) {
firstEdge = centers[0] - (centers[1] - centers[0]) / 2;
} else {
// 只有一个点时,给一个合理的固定偏移
firstEdge = centers[0] - 40;
}
graphics.push({
type: 'line',
shape: {x1: firstEdge, y1: yStemStart, x2: firstEdge, y2: yStemEnd},
style: {stroke: '#bbb', lineWidth: 1.5},
silent: true,
z: 120,
zlevel: 10
});
// 相邻两点之间的中点
for (let i = 0; i < centers.length - 1; i++) {
const mid = (centers[i] + centers[i + 1]) / 2;
graphics.push({
type: 'line',
shape: {x1: mid, y1: yStemStart, x2: mid, y2: yStemEnd},
style: {stroke: '#bbb', lineWidth: 1.5},
silent: true,
z: 120,
zlevel: 10
});
}
// 右侧最后一个边界线:对称估算半步并绘制(补齐“最后那条线”)
let lastEdge;
if (centers.length >= 2) {
lastEdge = centers[centers.length - 1] + (centers[centers.length - 1] - centers[centers.length - 2]) / 2;
} else {
lastEdge = centers[0] + 40;
}
graphics.push({
type: 'line',
shape: {x1: lastEdge, y1: yStemStart, x2: lastEdge, y2: yStemEnd},
style: {stroke: '#bbb', lineWidth: 1.5},
silent: true,
z: 120,
zlevel: 10
});
}
const prev = Array.isArray(this.currentGraphicElements) ? this.currentGraphicElements : [];
const merged = prev.concat(graphics);
this.currentGraphicElements = merged;
this.$nextTick(() => {
if (this.myChart) {
this.myChart.setOption({graphic: {elements: merged}}, {replaceMerge: ['graphic']});
}
});
},
// 图内根据 x 和 y2 渲染 SVG 纹理(每段使用该 x 在 stackedAreas 中最浅层 y2 对应的 svg),从顶部填充到 y2 深度
async drawTopBandSvg(xAxisLabels, chartConfig, stackedAreas) {
if (!this.myChart || !Array.isArray(xAxisLabels) || !stackedAreas) return;
// 确保图表已完全渲染,检查容器尺寸
const chartDom = this.$refs.chartRef;
if (!chartDom || chartDom.offsetWidth === 0 || chartDom.offsetHeight === 0) {
console.warn('图表容器尺寸无效,跳过绘制顶部SVG纹理');
return;
}
// 计算每个类目的像素中心
const centers = [];
for (let i = 0; i < xAxisLabels.length; i++) {
const cx = this.myChart.convertToPixel({xAxisIndex: 0}, i);
if (cx != null) centers.push(cx);
}
if (centers.length === 0) return;
// 计算段边界(第一个左边界 + 各中点)
let firstEdge;
if (centers.length >= 2) {
firstEdge = centers[0] - (centers[1] - centers[0]) / 2;
} else {
firstEdge = centers[0] - 40;
}
const boundaries = [firstEdge];
for (let i = 0; i < centers.length - 1; i++) {
boundaries.push((centers[i] + centers[i + 1]) / 2);
}
// 图内纵向像素范围:从图内顶部到 y2 所在像素
const yMinPx = this.myChart.convertToPixel({yAxisIndex: 0}, chartConfig.yAxis.min);
const yMaxPx = this.myChart.convertToPixel({yAxisIndex: 0}, chartConfig.yAxis.max);
if (yMinPx == null || yMaxPx == null) return;
const yTopPx = Math.min(yMinPx, yMaxPx);
const plotHeight = Math.abs(yMaxPx - yMinPx);
// 摊平 stackedAreas 以便按 x 查找图层(保留全部层,用于全部渲染)
const allLayers = [];
if (Array.isArray(stackedAreas)) {
for (const l of stackedAreas) if (l) allLayers.push(l);
} else {
for (const layers of Object.values(stackedAreas)) {
if (Array.isArray(layers)) allLayers.push(...layers.filter(Boolean));
}
}
const graphics = [];
// 仅在每口井的最后一列标注层名称
const depthIntervals = this.mockData?.wellData?.depthIntervals || [];
const lastIndexByJh = new Map();
for (let idx = 0; idx < depthIntervals.length; idx++) {
const jhVal = depthIntervals[idx]?.jh;
if (jhVal != null) lastIndexByJh.set(jhVal, idx);
}
for (let i = 0; i < xAxisLabels.length; i++) {
const xLabel = String(xAxisLabels[i]);
const left = boundaries[i];
const right = (i < boundaries.length - 1) ? boundaries[i + 1] : left + (centers[1] ? (centers[1] - centers[0]) : 80);
const width = Math.max(10, right - left);
// 找到包含该 x 的所有层,提取该 x 对应点的 y2;若有重叠(相同 y2)则只保留一次
const y2ToLayer = new Map();
for (const layer of allLayers) {
const pts = Array.isArray(layer?.points) ? layer.points : [];
const p = pts.find(p0 => String(p0?.x) === xLabel);
if (!p) continue;
const y2val = Number(p?.y2 ?? layer?.sjdjsd);
if (Number.isNaN(y2val)) continue;
if (!y2ToLayer.has(y2val)) y2ToLayer.set(y2val, layer);
}
const sortedY2 = Array.from(y2ToLayer.keys()).sort((a, b) => a - b);
let prevBottom = null; // 上一个片段的底(y2),为空则从图内顶部开始
for (const y2val of sortedY2) {
const layer = y2ToLayer.get(y2val);
const yTopDepth = prevBottom == null ? chartConfig?.yAxis?.min : prevBottom;
const yBottomDepth = y2val;
const yPixTop = this.myChart.convertToPixel({yAxisIndex: 0}, yTopDepth);
const yPixBottom = this.myChart.convertToPixel({yAxisIndex: 0}, yBottomDepth);
if (yPixTop == null || yPixBottom == null) {
prevBottom = y2val;
continue;
}
const rectTop = Math.min(yPixTop, yPixBottom);
const rectBottom = Math.max(yPixTop, yPixBottom);
const y1pix = Math.max(yTopPx, rectTop);
const y2pix = Math.min(Math.max(yMinPx, yMaxPx), rectBottom);
const height = Math.max(0, y2pix - y1pix);
if (height > 0) {
let fill = '#d0d3d8';
if (layer?.svg) {
try {
const img = await this.createSvgImage(layer.svg);
fill = {image: img, repeat: 'repeat'};
} catch (e) { /* ignore */
}
}
graphics.push({
type: 'rect',
shape: {x: left, y: y1pix, width, height},
style: {fill, stroke: 'rgba(0,0,0,0.12)', lineWidth: 1},
silent: true,
z: -10,
zlevel: 1,
__band: true
});
}
prevBottom = y2val;
}
}
// 合并到当前 graphic 上:先移除旧 band,再追加新的,避免重复渲染
const prev = Array.isArray(this.currentGraphicElements) ? this.currentGraphicElements : [];
const kept = prev.filter(el => !el.__band);
const merged = kept.concat(graphics);
this.currentGraphicElements = merged;
this.$nextTick(() => {
if (this.myChart) {
this.myChart.setOption({graphic: {elements: merged}}, {replaceMerge: ['graphic']});
}
});
},
// 导出图表为图片
exportChart() {
if (!this.myChart) {
this.$message.warning('图表尚未加载完成,请稍候再试');
return;
}
try {
// 获取图表图片数据
const dataURL = this.myChart.getDataURL({
type: 'png',
pixelRatio: 2, // 提高图片清晰度
backgroundColor: '#fff'
});
// 创建下载链接
const link = document.createElement('a');
const fileName = this.jh ? `直方图_${this.jh}_${new Date().getTime()}.png` : `直方图_${new Date().getTime()}.png`;
link.href = dataURL;
link.download = fileName;
link.style.display = 'none';
// 触发下载
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
this.$message.success('图表导出成功');
} catch (error) {
console.error('导出图表失败:', error);
this.$message.error('导出图表失败,请稍候再试');
}
}
},
};
</script>
<style lang="scss" scoped>
/* 容器样式优化 */
.chart-container {
width: 100%;
min-height: calc(100vh - 140px);
padding: 0 10px;
margin: 0;
box-sizing: border-box;
display: flex;
flex-direction: column;
position: relative;
overflow: auto;
}
.chart-layout {
display: flex;
flex: 1;
width: 100%;
gap: 5px;
align-items: stretch;
justify-content: flex-start;
overflow: hidden;
}
.strata-legend {
width: 240px;
min-width: 240px;
max-height: 100%;
padding: 16px;
border-radius: 16px;
background: #fff;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08);
border: 1px solid rgba(15, 23, 42, 0.06);
display: flex;
flex-direction: column;
gap: 12px;
}
.legend-header {
font-size: 16px;
font-weight: 600;
color: #1f2937;
border-bottom: 1px solid rgba(15, 23, 42, 0.08);
padding-bottom: 8px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.legend-list {
overflow-y: auto;
flex: 1;
display: flex;
flex-direction: column;
gap: 1px;
padding-right: 4px;
}
.legend-item {
display: flex;
align-items: center;
gap: 5px;
padding: 0px 4px;
border-radius: 8px;
transition: background 0.2s ease;
}
.legend-item:hover {
background-color: rgba(59, 130, 246, 0.08);
}
.legend-icon {
width: 40px;
height: 24px;
border-radius: 6px;
border: 1px solid rgba(15, 23, 42, 0.15);
background-color: #d1d5db;
background-size: cover !important;
}
.legend-label {
flex: 1;
font-size: 13px;
font-weight: 500;
color: #1f2937;
}
/* 井号显示样式 */
.well-number-display {
position: absolute;
top: 16px;
left: 16px;
z-index: 5;
background: transparent;
padding: 8px 12px;
display: flex;
align-items: center;
gap: 6px;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
transition: all 0.3s ease;
opacity: 0.9;
}
/* 导出按钮样式 */
::v-deep .export-btn {
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%) !important;
color: #fff !important;
border: none !important;
border-radius: 6px !important;
padding: 6px 14px !important;
font-size: 13px !important;
font-weight: 500 !important;
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3) !important;
transition: all 0.3s ease !important;
}
::v-deep .export-btn:hover:not(:disabled) {
background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%) !important;
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.4) !important;
transform: translateY(-1px) !important;
}
::v-deep .export-btn:active:not(:disabled) {
transform: translateY(0) !important;
box-shadow: 0 2px 6px rgba(59, 130, 246, 0.3) !important;
}
::v-deep .export-btn:disabled {
background: #9ca3af !important;
cursor: not-allowed !important;
opacity: 0.6 !important;
box-shadow: none !important;
}
::v-deep .export-btn .el-icon-download {
color: #fff !important;
}
.well-number-display:hover {
opacity: 1;
}
.well-label {
color: #6b7280;
font-size: 13px;
font-weight: 500;
white-space: nowrap;
}
.well-number {
color: #3b82f6;
font-size: 14px;
font-weight: 600;
}
/* 图表容器样式 */
.chart {
flex: 1;
width: 100%;
max-width: 100%;
height: 100%;
min-height: 400px;
display: block;
border-radius: 16px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
border: 1px solid rgba(255, 255, 255, 0.8);
backdrop-filter: blur(10px);
transition: all 0.3s ease;
}
.chart:hover {
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.15);
/* transform: translateY(-2px); */
}
/* 加载状态样式 */
.loading-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.95);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 10;
border-radius: 16px;
backdrop-filter: blur(10px);
}
.loading-spinner {
width: 48px;
height: 48px;
border: 4px solid #f3f4f6;
border-top: 4px solid #3b82f6;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 16px;
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.2);
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
/* 响应式优化 */
@media (max-width: 768px) {
.chart-container {
padding: 0 10px;
}
.chart {
min-height: 300px;
border-radius: 12px;
}
.export-btn {
padding: 4px 8px;
font-size: 12px;
}
}
/* 深色模式支持 */
@media (prefers-color-scheme: dark) {
.chart {
/* background: linear-gradient(135deg, #1f2937 0%, #111827 100%); */
/* box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4); */
border: 1px solid rgba(255, 255, 255, 0.1);
}
/*
.chart:hover {
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.5);
} */
.loading-overlay {
background: rgba(17, 24, 39, 0.95);
}
}
/* 动画效果 */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.chart-container {
animation: fadeIn 0.6s ease-out;
}
</style>
<template>
<div class="chart-container">
<div class="chart-layout">
<div id="mainzftdj" class="chart" ref="chartRef"></div>
<aside v-if="legendItems && legendItems.length" class="strata-legend">
<div class="legend-header">
<span>层位图例</span>
<el-button class="export-btn" @click="exportChart" :disabled="loading || !myChart" title="导出图表为图片"
icon="el-icon-download" size="small">
导出
</el-button>
</div>
<div class="legend-list">
<div v-for="(item, index) in legendItems" :key="item.name || index" class="legend-item">
<div class="legend-icon" :style="getLegendSwatchStyle(item)"></div>
<span class="legend-label">{{ item.name || '-' }}</span>
</div>
</div>
</aside>
</div>
<div v-if="loading" class="loading-overlay">
<div class="loading-spinner"></div>
<span>加载中...</span>
</div>
</div>
</template>
<script>
import * as echarts from "echarts";
import { getdjZft } from "@/api/optimization/initialization";
import { getljqxData } from "@/api/system/cjsjLas";
import { listLjSssjSd } from "@/api/optimization/ljSssjSd";
import { text } from "d3";
export default {
name: "HistogramGraph",
props: {
jh: {
type: String,
default: "",
},
jhs: {
type: String,
default: ''
},
// 美化配置选项
theme: {
type: String,
default: "modern", // modern, elegant, vibrant
validator: value => ["modern", "elegant", "vibrant"].includes(value)
}
},
data() {
return {
mockData: {},
legendItems: [],
myChart: null,
initRetryCount: 0,
maxRetryCount: 5,
resizeObserver: null,
loading: false,
debounceTimer: null,
lastStackedAreas: null,
lastChartConfig: null,
lastXAxisLabels: null,
lastDepthIntervals: null,
currentGraphicElements: [],
// 录井多曲线数据(钻时/扭矩/立压/钻压/转速/泵冲/入口流量)
curveData: null,
};
},
computed: {
// 根据主题获取颜色配置
colorScheme() {
const schemes = {
modern: {
primary: "#3B82F6",
secondary: "#10B981",
accent: "#F59E0B",
background: "#F8FAFC",
text: "#1F2937",
border: "#E5E7EB",
gradient: {
start: "#3B82F6",
end: "#1D4ED8"
}
},
elegant: {
primary: "#6366F1",
secondary: "#8B5CF6",
accent: "#EC4899",
background: "#FAFAFA",
text: "#374151",
border: "#D1D5DB",
gradient: {
start: "#6366F1",
end: "#4F46E5"
}
},
vibrant: {
primary: "#EF4444",
secondary: "#06B6D4",
accent: "#F97316",
background: "#FEFEFE",
text: "#111827",
border: "#F3F4F6",
gradient: {
start: "#EF4444",
end: "#DC2626"
}
}
};
return schemes[this.theme];
},
legendPanelWidth() {
return this.legendItems && this.legendItems.length ? 260 : 0;
}
},
watch: {
jh: {
handler(newVal) {
// 不自动刷新,等待父级触发 loadData
},
immediate: false
},
legendItems: {
handler() {
this.$nextTick(() => {
const chartDom = this.$refs.chartRef;
if (chartDom) {
this.setChartDimensions(chartDom);
this.performResize();
}
});
},
deep: true
}
},
mounted() {
// 初始化空图表,不拉数据;等待父组件触发 loadData
this.initChart();
this.setupEventListeners();
},
beforeDestroy() {
this.cleanup();
},
methods: {
// 初始化图表
async initChart() {
try {
this.loading = true;
const chartDom = this.$refs.chartRef;
if (!chartDom) {
throw new Error("未找到图表容器 DOM");
}
this.setChartDimensions(chartDom);
// 重试机制优化
if (this.shouldRetry()) {
this.scheduleRetry();
return;
}
this.resetRetryCount();
this.disposeChart();
this.createChart(chartDom);
// 并行获取直方图数据 + 多曲线数据
const [mockData, curveData] = await Promise.all([
this.getList(),
this.getCurveData()
]);
this.curveData = curveData;
if (!this.hasValidData(mockData)) {
this.renderEmpty(chartDom);
return;
}
await this.renderRealData(mockData);
} catch (error) {
console.error("图表初始化失败:", error);
this.handleError(error);
} finally {
this.loading = false;
}
},
// 设置图表尺寸
setChartDimensions(chartDom) {
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
const containerWidth = this.$el?.clientWidth || viewportWidth;
const rect = this.$el?.getBoundingClientRect();
const topOffset = rect ? rect.top : 0;
const legendWidth = this.legendPanelWidth || 0;
const containerPadding = 20; // chart-container 水平 padding 之和
const panelGap = legendWidth ? 5 : 0; // 与右侧图例的间距
const safetyMargin = 12; // 额外预留
const widthPadding = containerPadding + panelGap + legendWidth + safetyMargin;
const availableWidth = Math.max(360, containerWidth - widthPadding);
const verticalPadding = 40;
const heightByViewport = viewportHeight - topOffset - verticalPadding;
const fallbackHeight = this.$el?.clientHeight || viewportHeight;
const availableHeight = Math.max(360, heightByViewport, fallbackHeight - 20);
chartDom.style.width = `${availableWidth}px`;
chartDom.style.height = `${availableHeight}px`;
chartDom.offsetHeight; // 强制重排
},
// 检查是否需要重试
shouldRetry() {
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
const availableHeight = viewportHeight - 120;
const availableWidth = viewportWidth - 80;
return (availableWidth <= 0 || availableHeight <= 0) && this.initRetryCount < this.maxRetryCount;
},
// 安排重试
scheduleRetry() {
this.initRetryCount++;
setTimeout(() => this.initChart(), 500);
},
// 重置重试计数
resetRetryCount() {
this.initRetryCount = 0;
},
// 销毁图表
disposeChart() {
if (this.myChart) {
this.myChart.dispose();
}
},
// 创建图表实例
createChart(chartDom) {
this.myChart = echarts.init(chartDom, null, {
renderer: 'canvas',
useDirtyRect: true
});
},
// 检查数据有效性
hasValidData(mockData) {
return mockData &&
mockData.wellData &&
mockData.wellData.depthLine &&
mockData.wellData.depthLine.length > 0;
},
// 设置事件监听器
setupEventListeners() {
window.addEventListener("resize", this.handleResize);
this.observeParentResize();
},
// 清理资源
cleanup() {
window.removeEventListener("resize", this.handleResize);
if (this.myChart) {
this.myChart.dispose();
}
if (this.resizeObserver) {
this.resizeObserver.disconnect();
}
if (this.debounceTimer) {
clearTimeout(this.debounceTimer);
}
},
// 观察父容器大小变化
observeParentResize() {
const parentDom = this.$refs.chartRef?.parentElement;
if (parentDom && window.ResizeObserver) {
this.resizeObserver = new ResizeObserver(() => {
this.debouncedResize();
});
this.resizeObserver.observe(parentDom);
}
},
// 获取数据
async getList() {
try {
const res = await getdjZft({
jhs: `${this.jh},${this.jhs}`
});
this.mockData = res?.mockData || {};
const legendList = Array.isArray(res?.tlList)
? res.tlList
: Array.isArray(res?.mockData?.tlList)
? res.mockData.tlList
: [];
this.legendItems = legendList;
return this.mockData;
} catch (error) {
console.error("获取数据失败:", error);
throw error;
}
},
// 获取当前主井的录井多曲线数据(钻时等),用于在同一张图上叠加折线
async getCurveData() {
// 优先用 props.jh;如果没有,再从 jhs 里取第一个井号做回退
let targetJh = this.jh;
if (!targetJh && this.jhs) {
const arr = String(this.jhs)
.split(",")
.map((s) => s.trim())
.filter((s) => s);
if (arr.length) {
targetJh = arr[0];
}
}
console.log("HistogramGraph.getCurveData => jh:", targetJh, "props:", {
jh: this.jh,
jhs: this.jhs,
});
if (!targetJh) {
console.warn("HistogramGraph.getCurveData: 无有效井号,跳过获取曲线数据");
return null;
}
try {
const [ljqxRes, ljSssjRes] = await Promise.all([
getljqxData({ jh: targetJh }),
listLjSssjSd({ jh: targetJh, pageNum: 1, pageSize: 999999 }),
]);
console.log("HistogramGraph.getCurveData 接口返回:", {
ljqxRes,
ljSssjRes,
});
const processed = this.processCurveData(ljqxRes, ljSssjRes);
console.log("HistogramGraph.getCurveData 处理后曲线数据:", processed);
return processed;
} catch (e) {
console.error("获取录井多曲线数据失败:", e);
return null;
}
},
// 处理录井多曲线数据,结构参考 DrillingTimeChart.vue
processCurveData(ljqxRes, ljSssjRes) {
console.log("HistogramGraph.processCurveData 输入原始数据:", {
ljqxRes,
ljSssjRes,
});
const processArrayData = (dataList, fieldName) => {
if (!dataList || !Array.isArray(dataList) || dataList.length === 0) {
return [];
}
// 对象数组 [{dept: xxx, value: xxx}, ...]
if (typeof dataList[0] === "object" && dataList[0].hasOwnProperty("dept")) {
return dataList.map((item) => ({
depth: item.dept || item.depth,
value: item[fieldName] || item.value || item,
}));
}
// 普通数组,深度从录井整米数据里取
return dataList.map((value, index) => {
const depth =
ljSssjRes && ljSssjRes.rows && ljSssjRes.rows[index]
? ljSssjRes.rows[index].js
: null;
return { depth, value };
});
};
// 钻时:录井整米数据 rows.zs
let drillingTimeData = [];
if (ljSssjRes && ljSssjRes.rows && Array.isArray(ljSssjRes.rows)) {
drillingTimeData = ljSssjRes.rows.map((item) => ({
depth: item.js,
value: item.zs,
}));
}
const torqueData = processArrayData(ljqxRes.njList, "nj");
const standpipePressureData = processArrayData(ljqxRes.lyList, "ly");
const drillingPressureData = processArrayData(ljqxRes.zyList, "zy");
const rpmData = processArrayData(ljqxRes.zs1List, "zs1");
const rkllData = processArrayData(ljqxRes.rkllList, "rkll");
// 泵冲:录井整米数据 rows.zbc
let pumpStrokeData = [];
if (ljSssjRes && ljSssjRes.rows && Array.isArray(ljSssjRes.rows)) {
pumpStrokeData = ljSssjRes.rows.map((item) => ({
depth: item.js,
value: item.zbc,
}));
}
const result = {
drillingTime: drillingTimeData,
torque: torqueData,
standpipePressure: standpipePressureData,
drillingPressure: drillingPressureData,
rpm: rpmData,
pumpStroke: pumpStrokeData,
rkllData: rkllData,
};
console.log("HistogramGraph.processCurveData 输出:", {
drillingTimeLen: drillingTimeData.length,
torqueLen: torqueData.length,
standpipePressureLen: standpipePressureData.length,
drillingPressureLen: drillingPressureData.length,
rpmLen: rpmData.length,
pumpStrokeLen: pumpStrokeData.length,
rkllLen: rkllData.length,
result,
});
return result;
},
// 供父组件在切换到本tab时调用
async loadData() {
await this.initChart();
},
// 处理窗口大小变化
handleResize() {
this.debouncedResize();
},
// 防抖处理resize
debouncedResize() {
if (this.debounceTimer) {
clearTimeout(this.debounceTimer);
}
this.debounceTimer = setTimeout(() => {
this.performResize();
}, 200);
},
// 执行resize操作
performResize() {
if (this.myChart) {
const chartDom = this.$refs.chartRef;
if (chartDom) {
this.setChartDimensions(chartDom);
}
this.$nextTick(() => {
if (this.myChart) {
this.myChart.resize();
if (this.lastStackedAreas && this.lastChartConfig && this.lastXAxisLabels) {
this.drawStratumLabels(this.lastStackedAreas, this.lastChartConfig, this.lastXAxisLabels);
}
if (this.lastDepthIntervals && this.lastXAxisLabels && this.lastChartConfig) {
this.drawJhSeparators(this.lastDepthIntervals, this.lastXAxisLabels, this.lastChartConfig);
}
if (this.lastXAxisLabels && this.lastChartConfig) {
this.drawCategoryEdgeLines(this.lastXAxisLabels, this.lastChartConfig);
this.drawTopBandSvg(this.lastXAxisLabels, this.lastChartConfig, this.lastStackedAreas);
}
}
});
}
},
// 刷新图表
async refreshChart() {
if (this.myChart) {
await this.initChart();
}
},
// 渲染真实数据
async renderRealData(mockData) {
try {
const { wellData } = mockData;
console.log(wellData, 'wellData');
if (this.curveData) {
console.log("HistogramGraph.renderRealData 当前曲线数据:", this.curveData);
} else {
console.warn("HistogramGraph.renderRealData: 当前 curveData 为空或未获取到");
}
const xAxisLabels = wellData.depthLine.map((point) => point.x);
try {
window.__hist_xLabels = xAxisLabels;
} catch (e) {
}
const option = {
...this.getDefaultChartOption(),
xAxis: this.createXAxis(xAxisLabels),
yAxis: this.createYAxis(mockData.chartConfig),
series: this.createSeries(wellData)
};
// 如果有录井多曲线数据,则在同一 grid 中叠加一组使用 value 型 x 轴的折线
if (this.curveData) {
this.extendWithCurves(option, mockData.chartConfig);
}
const chartDom = this.$refs.chartRef;
if (chartDom) {
this.setChartDimensions(chartDom);
}
this.myChart.setOption(option, true);
// 确保图表完全渲染后再resize和绘制
this.$nextTick(() => {
// 先resize确保图表尺寸正确
this.myChart.resize();
// 使用 requestAnimationFrame 确保浏览器完成渲染后再绘制
requestAnimationFrame(() => {
requestAnimationFrame(() => {
// 再次确认尺寸,确保图表已完全渲染
const chartDom = this.$refs.chartRef;
if (chartDom && chartDom.offsetWidth > 0 && chartDom.offsetHeight > 0) {
// 再次resize确保尺寸准确
this.myChart.resize();
// 保存数据用于后续resize时重新绘制
this.lastStackedAreas = wellData.stackedAreas;
this.lastChartConfig = mockData.chartConfig;
this.lastXAxisLabels = xAxisLabels;
this.lastDepthIntervals = wellData.depthIntervals || [];
// 绘制所有图形元素
this.drawStratumLabels(this.lastStackedAreas, this.lastChartConfig, this.lastXAxisLabels);
this.drawJhSeparators(this.lastDepthIntervals, this.lastXAxisLabels, this.lastChartConfig);
this.drawCategoryEdgeLines(this.lastXAxisLabels, this.lastChartConfig);
this.drawTopBandSvg(this.lastXAxisLabels, this.lastChartConfig, this.lastStackedAreas);
} else {
// 如果尺寸还未确定,延迟重试
setTimeout(() => {
if (this.myChart) {
this.myChart.resize();
this.lastStackedAreas = wellData.stackedAreas;
this.lastChartConfig = mockData.chartConfig;
this.lastXAxisLabels = xAxisLabels;
this.lastDepthIntervals = wellData.depthIntervals || [];
this.drawStratumLabels(this.lastStackedAreas, this.lastChartConfig, this.lastXAxisLabels);
this.drawJhSeparators(this.lastDepthIntervals, this.lastXAxisLabels, this.lastChartConfig);
this.drawCategoryEdgeLines(this.lastXAxisLabels, this.lastChartConfig);
this.drawTopBandSvg(this.lastXAxisLabels, this.lastChartConfig, this.lastStackedAreas);
}
}, 150);
}
});
});
});
} catch (error) {
console.error("渲染数据失败:", error);
this.handleError(error);
}
},
// 获取默认图表配置
getDefaultChartOption() {
const colors = this.colorScheme;
return {
title: { text: "", subtext: "" },
aria: { enabled: false },
backgroundColor: colors.background,
tooltip: {
trigger: "axis",
axisPointer: {
type: "cross",
label: { backgroundColor: colors.primary, show: false },
crossStyle: { color: colors.border }
},
backgroundColor: 'rgba(255,255,255,0.95)',
borderColor: colors.border,
borderWidth: 1,
textStyle: { color: colors.text },
extraCssText: 'box-shadow: 0 4px 12px rgba(0,0,0,0.15); border-radius: 8px;',
formatter: (params) => {
if (!params || params.length === 0) return '';
const dataIndex = params[0].dataIndex;
const depthInterval = this.mockData?.wellData?.depthIntervals?.[dataIndex];
if (!depthInterval) {
// 如果没有depthInterval数据,使用默认格式
let result = `<div style="font-weight: 600; margin-bottom: 8px; color: ${colors.primary};">${params[0].axisValue}</div>`;
params.forEach(param => {
const color = param.color;
result += `<div style="margin: 4px 0;">
<span style="display: inline-block; width: 10px; height: 10px; background-color: ${color}; border-radius: 50%; margin-right: 8px;"></span>
<span style="font-weight: 500;">${param.seriesName}:</span>
<span style="margin-left: 8px; font-weight: 600;">${param.value}</span>
</div>`;
});
return result;
}
// 显示井号信息
let result = `<div style="font-weight: 600; margin-bottom: 8px; color: ${colors.primary};">${depthInterval.jh || depthInterval.x}</div>`;
// 显示详细信息
const modelLabel = this.formatXAxisLabel(depthInterval.x || '-');
result += `<div style="margin: 4px 0; padding: 4px 0; border-top: 1px solid ${colors.border};">
<div style="margin: 2px 0;"><span style="font-weight: 500;">型号:</span> <span style="margin-left: 8px; font-weight: 600;">${modelLabel}</span></div>
<div style="margin: 2px 0;"><span style="font-weight: 500;">尺寸:</span> <span style="margin-left: 8px; font-weight: 600;">${depthInterval.ztcc || '-'}</span></div>
<div style="margin: 2px 0;"><span style="font-weight: 500;">进尺mm:</span> <span style="margin-left: 8px; font-weight: 600;">${depthInterval.jc || '-'}</span></div>
<div style="margin: 2px 0;"><span style="font-weight: 500;">深度区间:</span> <span style="margin-left: 8px; font-weight: 600;">${depthInterval.interval || '-'}</span></div>
</div>`;
// 只显示非地层信息的系列数据(过滤掉地层相关的系列)
// params.forEach(param => {
// // 过滤掉地层相关的系列名称
// if (param.seriesName && !param.seriesName.includes('地层') && param.seriesName !== '占位') {
// const color = param.color;
// result += `<div style="margin: 4px 0;">
// <span style="display: inline-block; width: 10px; height: 10px; background-color: ${color}; border-radius: 50%; margin-right: 8px;"></span>
// <span style="font-weight: 500;">${param.seriesName}:</span>
// <span style="margin-left: 8px; font-weight: 600;">${param.value}</span>
// </div>`;
// }
// });
return result;
}
},
grid: {
top: 20,
left: "2%",
right: "3%",
bottom: "10%",
containLabel: true,
show: false,
},
animation: true,
animationDuration: 1500,
animationEasing: 'cubicOut',
animationDelay: function (idx) {
return idx * 100;
}
};
},
// 处理区域系列
async processAreaSeries(wellData, xAxisLabels) {
const stackedAreas = wellData?.stackedAreas || [];
// 兼容旧格式:数组(所有井已按全局顺序平铺)
if (Array.isArray(stackedAreas)) {
return await Promise.all(
stackedAreas
.filter(area => area.svg !== null)
.map(async (area) => {
let areaStyle = { opacity: 0.6 };
if (area.svg) {
try {
const svgImage = await this.createSvgImage(area.svg);
areaStyle.color = { image: svgImage, repeat: "repeat" };
} catch (error) {
console.error('SVG转换失败:', error);
}
}
return {
name: area.name,
type: "line",
xAxisIndex: 1,
z: -1,
stack: "总量",
symbol: "none",
showSymbol: false,
areaStyle: areaStyle,
lineStyle: { width: 0 },
data: area.points.map((point) => (point && point.y2 != null) ? point.y2 : point.y),
};
})
);
}
// 新格式:对象,key 为井号,每口井内是该井的地层数组
const totalLen = Array.isArray(xAxisLabels) ? xAxisLabels.length : 0;
console.log(totalLen, 'totalLen');
// 全局 x 标签 -> 索引数组 映射
const xToIndices = new Map();
console.log(xToIndices, 'xToIndices');
(xAxisLabels || []).forEach((x, idx) => {
const key = String(x);
if (!xToIndices.has(key)) xToIndices.set(key, []);
xToIndices.get(key).push(idx);
});
const buildNullArray = (len) => Array.from({ length: len }, () => null);
console.log(buildNullArray, 'buildNullArray');
const results = [];
for (const [jh, layers] of Object.entries(stackedAreas)) {
if (!Array.isArray(layers) || layers.length === 0) continue;
// 估算该井的横向覆盖范围:取该井所有层的所有点对应的全局索引的 min/max
const indicesForJh = [];
const missingInXAxis = [];
for (const area of layers) {
for (const p of (area?.points || [])) {
const arr = xToIndices.get(String(p?.x)) || [];
if (arr.length === 0) {
missingInXAxis.push({ x: p?.x, jh, areaName: area?.name });
} else {
indicesForJh.push(...arr);
}
}
}
const startIdx = Math.min(...indicesForJh);
const endIdx = Math.max(...indicesForJh);
for (const area of (layers || [])) {
if (area == null) continue;
let areaStyle = { opacity: 0.6 };
if (area.svg) {
try {
const svgImage = await this.createSvgImage(area.svg);
areaStyle.color = { image: svgImage, repeat: "repeat" };
} catch (error) {
console.error('SVG转换失败:', error);
}
}
const data = buildNullArray(totalLen);
// 把该层每个点的 y2/y 填到该点 x 对应的全局索引中,仅限落在该井的覆盖范围内
const outOfRangePoints = [];
for (const p of (area.points || [])) {
const yVal = (p && p.y2 != null) ? p.y2 : p?.y;
const cands = xToIndices.get(String(p?.x)) || [];
if (cands.length === 0) continue;
let placed = false;
cands.forEach(ix => {
if (ix >= startIdx && ix <= endIdx) {
data[ix] = yVal;
placed = true;
}
});
if (!placed) {
outOfRangePoints.push({ x: p?.x, cands, jh, areaName: area?.name, startIdx, endIdx });
}
}
results.push({
name: area.name,
type: "line",
xAxisIndex: 1,
z: -1,
stack: "总量",
symbol: "none",
showSymbol: false,
areaStyle: areaStyle,
lineStyle: { width: 0 },
data
});
// 仅在有异常时输出调试信息,方便定位 x 匹配问题
if (missingInXAxis.length > 0 || outOfRangePoints.length > 0) {
// 使用 console.group 让日志更清晰
console.groupCollapsed && console.groupCollapsed(`层定位告警: 井号=${jh}`);
if (missingInXAxis.length > 0) {
console.warn('x 不在 xAxisLabels 中:', missingInXAxis);
}
if (outOfRangePoints.length > 0) {
console.warn('x 命中但不在井段范围内:', outOfRangePoints);
console.info('井段范围:', { jh, startIdx, endIdx });
}
console.groupEnd && console.groupEnd();
}
}
}
return results;
},
// 创建X轴配置
createXAxis(xAxisLabels) {
const colors = this.colorScheme;
const formatLabel = (value) => this.formatXAxisLabel(value);
return [
{
type: "category",
boundaryGap: true,
position: "top",
data: xAxisLabels,
axisLabel: {
interval: 0,
rotate: -90,
margin: 30,
align: "center",
fontSize: 12,
color: colors.text,
fontWeight: 500,
formatter: formatLabel
},
axisTick: {
alignWithLabel: true,
length: 6,
lineStyle: { color: colors.border }
},
splitLine: { show: false },
axisLine: {
show: true,
lineStyle: {
color: colors.border,
width: 2
}
}
},
{
type: "category",
boundaryGap: false,
position: "top",
show: false,
data: xAxisLabels
},
];
},
// 创建Y轴配置
createYAxis(chartConfig) {
const colors = this.colorScheme;
return [
{
type: "value",
name: "井深(m)",
nameTextStyle: {
color: colors.text,
fontSize: 13,
fontWeight: 600,
padding: [0, 0, 0, 8]
},
min: chartConfig.yAxis.min,
max: chartConfig.yAxis.max,
interval: chartConfig.yAxis.interval,
inverse: true,
axisLabel: {
formatter: "{value}",
fontSize: 12,
color: colors.text,
fontWeight: 500
},
splitLine: {
show: true,
lineStyle: {
color: colors.border,
type: 'dashed',
opacity: 0.3
}
},
axisLine: {
show: true,
lineStyle: {
color: colors.border,
width: 2
}
},
axisTick: {
show: true,
lineStyle: { color: colors.border }
}
},
{
type: "value",
name: "深度(m)",
nameTextStyle: {
color: colors.text,
fontSize: 13,
fontWeight: 600,
padding: [0, 8, 0, 55]
},
min: chartConfig.yAxis2.min,
max: chartConfig.yAxis2.max,
interval: chartConfig.yAxis2.interval,
position: "right",
axisLabel: {
formatter: "{value}",
fontSize: 12,
color: colors.text,
fontWeight: 500
},
splitLine: { show: false },
axisLine: {
show: true,
lineStyle: {
color: colors.border,
width: 2
}
},
axisTick: {
show: true,
lineStyle: { color: colors.border }
}
},
];
},
// 创建系列配置
createSeries(wellData, areaSeries) {
const colors = this.colorScheme;
return [
{
name: "井深数据",
type: "line",
zlevel: 25,
yAxisIndex: 1,
symbol: "circle",
symbolSize: 10,
itemStyle: {
color: colors.accent,
borderColor: '#fff',
borderWidth: 3,
shadowColor: 'rgba(0,0,0,0.2)',
shadowBlur: 8
},
lineStyle: {
color: colors.accent,
width: 3,
type: 'solid',
shadowColor: 'rgba(0,0,0,0.1)',
shadowBlur: 4
},
label: {
show: true,
position: "top",
formatter: "{c}",
fontSize: 13,
fontWeight: 600,
color: colors.accent,
backgroundColor: "rgba(255,255,255,0.95)",
padding: [6, 8],
borderRadius: 6,
borderColor: colors.accent,
borderWidth: 2,
shadowColor: 'rgba(0,0,0,0.1)',
shadowBlur: 4
},
data: wellData.depthLine.map((point) => point.y),
},
{
name: "占位",
type: "bar",
zlevel: 20,
stack: "total",
silent: true,
barWidth: "20%",
itemStyle: {
borderColor: "transparent",
color: "transparent"
},
emphasis: {
itemStyle: {
borderColor: "transparent",
color: "transparent"
}
},
data: wellData.depthIntervals.map((item) => item.placeholder),
},
{
name: "深度区间",
type: "bar",
zlevel: 20,
stack: "total",
barWidth: "15%",
label: {
show: true,
position: 'insideTop',
color: '#fff',
fontSize: 11,
fontWeight: 600,
backgroundColor: 'rgba(0,0,0,0.1)',
padding: [2, 3],
borderRadius: 4,
borderColor: 'rgba(255,255,255,0.3)',
borderWidth: 1,
formatter: (params) => {
const di = wellData.depthIntervals[params.dataIndex];
const value = di?.jc ?? di?.interval;
if (value === undefined || value === null || value === '') return params.data;
return `${value}`;
}
},
itemStyle: {
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{ offset: 0, color: colors.gradient.start },
{ offset: 1, color: colors.gradient.end }
]
},
borderRadius: [4, 4, 0, 0],
shadowColor: 'rgba(0,0,0,0.2)',
shadowBlur: 8,
shadowOffsetY: 2
},
emphasis: {
itemStyle: {
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{ offset: 0, color: this.adjustColor(colors.gradient.start, 1.2) },
{ offset: 1, color: this.adjustColor(colors.gradient.end, 1.2) }
]
},
shadowBlur: 12,
shadowOffsetY: 4
}
},
data: wellData.depthIntervals.map((item) => {
const value = item?.jc ?? item?.interval;
return typeof value === 'number' ? value : Number(value) || 0;
}),
},
// 底部显示 interval 数值的透明条,用于放置底部标签
{
name: '深度区间-底部标签',
type: 'bar',
zlevel: 20,
// 与主柱重叠以便把标签放在柱底部区域内
barGap: '-100%',
barWidth: '20%',
itemStyle: {
color: 'transparent',
borderColor: 'transparent'
},
emphasis: { itemStyle: { color: 'transparent', borderColor: 'transparent' } },
label: {
show: false,
position: 'insideBottom',
color: '#fff',
fontSize: 11,
fontWeight: 600,
backgroundColor: 'rgba(0,0,0,0.1)',
padding: [2, 3],
borderRadius: 4,
borderColor: 'rgba(255,255,255,0.3)',
borderWidth: 1,
distance: 0,
formatter: (params) => {
const di = wellData.depthIntervals[params.dataIndex];
const value = di?.jc ?? di?.interval;
if (value === undefined || value === null || value === '') return params.data;
return `进尺 ${value}`;
}
},
data: wellData.depthIntervals.map((item) => {
const value = item?.jc ?? item?.interval;
return typeof value === 'number' ? value : Number(value) || 0;
})
}
];
},
// 在现有直方图配置上叠加 6 条曲线:统一 y 轴为深度,新建一条 value 型 x 轴承载数值
extendWithCurves(option, chartConfig) {
const cd = this.curveData;
if (!cd) {
console.warn("HistogramGraph.extendWithCurves: 无 curveData,直接返回");
return;
}
console.log("HistogramGraph.extendWithCurves 收到 curveData:", cd);
// 收集所有数值,确定新 x 轴范围
const collectValues = (arr) =>
(arr || [])
.map((p) => (p && p.value != null ? Number(p.value) : null))
.filter((v) => v != null && !Number.isNaN(v));
const allVals = [
...collectValues(cd.drillingTime),
...collectValues(cd.torque),
...collectValues(cd.standpipePressure),
...collectValues(cd.drillingPressure),
...collectValues(cd.rpm),
...collectValues(cd.pumpStroke),
...collectValues(cd.rkllData),
];
console.log(
"HistogramGraph.extendWithCurves 所有数值长度:",
allVals.length,
"示例前若干值:",
allVals.slice(0, 20)
);
if (!allVals.length) {
console.warn("HistogramGraph.extendWithCurves: 所有曲线数值为空,不生成折线");
return;
}
const minVal = Math.min(...allVals);
const maxVal = Math.max(...allVals);
const span = maxVal - minVal || 1;
const padding = span * 0.1;
const axisMin = minVal - padding;
const axisMax = maxVal + padding;
console.log("HistogramGraph.extendWithCurves x 轴范围:", {
minVal,
maxVal,
axisMin,
axisMax,
});
// 新增一条 value 型 x 轴,放在底部,专门给多曲线使用
const xAxisIndex = Array.isArray(option.xAxis) ? option.xAxis.length : 0;
if (!Array.isArray(option.xAxis)) option.xAxis = [];
option.xAxis.push({
type: "value",
position: "bottom",
name: "钻时 / 扭矩 / 立压 / 钻压 / 转速 / 泵冲 / 入口流量",
nameLocation: "center",
nameGap: 45,
min: axisMin,
max: axisMax,
axisLine: {
show: true,
lineStyle: {
color: "#9ca3af",
width: 1.5,
},
},
axisLabel: {
fontSize: 11,
color: "#4b5563",
},
splitLine: {
show: false,
},
});
if (!Array.isArray(option.series)) option.series = [];
const makeSeriesData = (arr) =>
(arr || [])
.filter(
(p) =>
p &&
p.depth != null &&
p.value != null &&
!Number.isNaN(Number(p.value))
)
.map((p) => [Number(p.value), p.depth])
.sort((a, b) => a[1] - b[1]); // 按深度排序
const pushCurve = (name, arr, color, yAxisIndex, lineWidth = 1) => {
const data = makeSeriesData(arr);
if (!data.length) return;
option.series.push({
name,
type: "line",
xAxisIndex,
yAxisIndex,
// 提高 zlevel,确保在柱子和地层之上渲染
zlevel: 30,
z: 30,
symbol: "none",
smooth: true,
lineStyle: {
color,
width: lineWidth,
},
data,
});
};
// 统一使用左侧深度轴(index 0),保证纵向坐标语义一致
pushCurve("钻时 (min/m)", cd.drillingTime, "#FF0000", 0, 2);
pushCurve("扭矩 (kN•m)", cd.torque, "#1E90FF", 0);
pushCurve("立压 (MPa)", cd.standpipePressure, "#32CD32", 0);
pushCurve("钻压 (kN)", cd.drillingPressure, "#FFD700", 0);
pushCurve("转速 (rpm)", cd.rpm, "#FF6347", 0);
pushCurve("泵冲", cd.pumpStroke, "#9370DB", 0);
pushCurve("入口流量", cd.rkllData, "#20B2AA", 0);
},
// 颜色调整工具方法
adjustColor(color, factor) {
if (color.startsWith('#')) {
const r = parseInt(color.slice(1, 3), 16);
const g = parseInt(color.slice(3, 5), 16);
const b = parseInt(color.slice(5, 7), 16);
const newR = Math.min(255, Math.round(r * factor));
const newG = Math.min(255, Math.round(g * factor));
const newB = Math.min(255, Math.round(b * factor));
return `rgb(${newR}, ${newG}, ${newB})`;
}
return color;
},
// 渲染空状态
renderEmpty(chartDom) {
const colors = this.colorScheme;
chartDom.innerHTML = `
<div class="empty-state" style="
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: ${colors.text};
font-size: 16px;
background: ${colors.background};
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
">
<div style="
font-size: 48px;
margin-bottom: 16px;
opacity: 0.6;
color: ${colors.primary};
">📊</div>
<div style="
font-size: 18px;
color: ${colors.text};
font-weight: 500;
">暂无数据</div>
<div style="
font-size: 14px;
color: ${colors.text};
opacity: 0.6;
margin-top: 8px;
">请检查数据配置</div>
</div>
`;
},
// 处理错误
handleError(error) {
console.error("图表错误:", error);
},
formatXAxisLabel(value) {
if (value == null) return "";
const str = String(value);
const idx = str.indexOf("-");
return idx === -1 ? str : str.slice(0, idx);
},
// 创建SVG图片
createSvgImage(svgString) {
return new Promise((resolve, reject) => {
const svgBlob = new Blob([svgString], { type: 'image/svg+xml;charset=utf-8' });
const svgUrl = URL.createObjectURL(svgBlob);
const img = new Image();
img.onload = () => {
URL.revokeObjectURL(svgUrl);
resolve(img);
};
img.onerror = (e) => {
URL.revokeObjectURL(svgUrl);
reject(e);
};
img.src = svgUrl;
});
},
// 为右侧图例生成纹理样式
getLegendSwatchStyle(item = {}) {
const baseStyle = {
backgroundColor: '#d1d5db',
backgroundRepeat: 'repeat',
backgroundSize: '36px 36px'
};
if (!item?.svg) return baseStyle;
const dataUrl = this.getSvgDataUrl(item.svg);
if (!dataUrl) return baseStyle;
return {
backgroundImage: `url("${dataUrl}")`,
backgroundRepeat: 'repeat',
backgroundSize: '36px 36px',
backgroundColor: 'transparent'
};
},
getSvgDataUrl(svgString) {
if (!svgString || typeof svgString !== 'string') return '';
try {
const compact = svgString.replace(/\s+/g, ' ').trim();
return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(compact)}`;
} catch (e) {
console.warn('SVG 编码失败:', e);
return '';
}
},
// 层位名称不再图内显示,仅清理旧的文字元素
drawStratumLabels() {
if (!this.myChart) return;
const elements = Array.isArray(this.currentGraphicElements)
? this.currentGraphicElements.filter(el => !el.__stratumLabel)
: [];
this.currentGraphicElements = elements;
this.$nextTick(() => {
if (this.myChart) {
this.myChart.setOption({ graphic: { elements } }, { replaceMerge: ['graphic'] });
}
});
},
// 根据 depthIntervals 的顺序按 jh 分段,绘制虚线与顶部井号
drawJhSeparators(depthIntervals, xAxisLabels, chartConfig) {
if (!this.myChart || !depthIntervals || depthIntervals.length === 0) return;
// 扫描 depthIntervals 顺序,形成段
const segments = [];
let currentJh = null;
let currentMin = Infinity;
let currentMax = -Infinity;
let started = false;
for (let i = 0; i < depthIntervals.length; i++) {
// 直接使用在 depthIntervals 中的顺序索引作为 x 轴位置,
// 以避免相同 x 标签造成的覆盖与错位
const { jh } = depthIntervals[i];
const idx = i;
if (!started) {
currentJh = jh;
currentMin = currentMax = idx;
started = true;
continue;
}
if (jh === currentJh) {
currentMin = Math.min(currentMin, idx);
currentMax = Math.max(currentMax, idx);
} else {
segments.push({
jh: currentJh,
startIdx: Math.min(currentMin, currentMax),
endIdx: Math.max(currentMin, currentMax)
});
// 开始新段
currentJh = jh;
currentMin = currentMax = idx;
}
}
if (started) {
segments.push({
jh: currentJh,
startIdx: Math.min(currentMin, currentMax),
endIdx: Math.max(currentMin, currentMax)
});
}
const graphics = [];
// 确保图表已完全渲染,检查容器尺寸
const chartDom = this.$refs.chartRef;
if (!chartDom || chartDom.offsetWidth === 0 || chartDom.offsetHeight === 0) {
console.warn('图表容器尺寸无效,跳过绘制井号分隔符');
return;
}
// 垂直范围(像素)——考虑 y 轴 inverse,取像素最小为顶部、最大为底部
const yMinPx = this.myChart.convertToPixel({ yAxisIndex: 0 }, chartConfig.yAxis.min);
const yMaxPx = this.myChart.convertToPixel({ yAxisIndex: 0 }, chartConfig.yAxis.max);
if (yMinPx === null || yMaxPx === null || isNaN(yMinPx) || isNaN(yMaxPx)) {
console.warn('convertToPixel 返回无效值,跳过绘制井号分隔符');
return;
}
const yTopPx = Math.min(yMinPx, yMaxPx);
const yBottomPx = Math.max(yMinPx, yMaxPx);
// 边界虚线(段与段之间),改为更浅的灰色虚线
for (let i = 0; i < segments.length - 1; i++) {
const leftEnd = segments[i].endIdx;
const rightStart = segments[i + 1].startIdx;
const pxLeft = this.myChart.convertToPixel({ xAxisIndex: 0 }, leftEnd);
const pxRight = this.myChart.convertToPixel({ xAxisIndex: 0 }, rightStart);
if (pxLeft === null || pxRight === null) {
console.warn('convertToPixel 返回 null 值,跳过绘制边界虚线');
continue;
}
const midX = (pxLeft + pxRight) / 2;
graphics.push({
type: 'line',
shape: { x1: midX, y1: yTopPx, x2: midX, y2: yBottomPx },
style: { stroke: '#bbb', lineDash: [8, 6], lineWidth: 1.5 },
silent: true,
z: 100,
zlevel: 10
});
}
// 每段顶部显示 jh(居中),带背景标签并做简单防重叠处理
let lastLabelRight = -Infinity;
let rowShift = 0; // 逐行上移避免覆盖
segments.forEach((seg, idx) => {
if (!seg.jh) return;
const pxStart = this.myChart.convertToPixel({ xAxisIndex: 0 }, seg.startIdx);
const pxEnd = this.myChart.convertToPixel({ xAxisIndex: 0 }, seg.endIdx);
if (pxStart === null || pxEnd === null) {
console.warn('convertToPixel 返回 null 值,跳过绘制井号标签');
return;
}
const midX = (pxStart + pxEnd) / 2;
let topY = yTopPx - 80; // 再上移,提升显示位置
// 估算标签宽度用于避免与上一个重叠
const fontSize = 12;
const paddingH = 6;
const estimatedWidth = seg.jh.length * fontSize * 0.6 + paddingH * 2 + 10;
const labelLeft = midX - estimatedWidth / 2;
const labelRight = midX + estimatedWidth / 2;
if (labelLeft < lastLabelRight) {
rowShift += 18; // 叠一行
} else {
rowShift = 0;
}
lastLabelRight = Math.max(lastLabelRight, labelRight);
topY -= rowShift;
const labelWidth = Math.max(estimatedWidth, 80);
const labelHeight = 22;
const pointerSize = 6;
graphics.push({
type: 'group',
x: midX - labelWidth / 2,
y: topY - labelHeight,
z: 101,
zlevel: 10,
silent: true,
children: [
{
type: 'rect',
shape: { x: 0, y: 0, width: labelWidth, height: labelHeight, r: 8 },
style: {
fill: '#2563eb',
stroke: '#1e40af',
lineWidth: 1.5,
shadowBlur: 6,
shadowColor: 'rgba(37,99,235,0.35)'
}
},
{
type: 'polygon',
shape: {
points: [
[labelWidth / 2 - pointerSize, labelHeight],
[labelWidth / 2 + pointerSize, labelHeight],
[labelWidth / 2, labelHeight + pointerSize]
]
},
style: { fill: '#2563eb', stroke: '#1e40af' }
},
{
type: 'text',
style: {
x: labelWidth / 2,
y: labelHeight / 2,
text: seg.jh,
fill: '#fff',
fontSize: fontSize + 1,
fontWeight: 700,
align: 'center',
verticalAlign: 'middle'
},
zlevel: 40
}
]
});
});
// 叠加到现有 graphic 上(保留原有 graphic)
const prevElements = Array.isArray(this.currentGraphicElements) ? this.currentGraphicElements : [];
const merged = prevElements.concat(graphics);
this.currentGraphicElements = merged;
this.$nextTick(() => {
if (this.myChart) {
this.myChart.setOption({ graphic: { elements: merged } }, { replaceMerge: ['graphic'] });
}
});
},
// 为每个 x 类目在左右各画一条黑色竖线(仅顶部短竖线)
drawCategoryEdgeLines(xAxisLabels, chartConfig) {
if (!this.myChart || !Array.isArray(xAxisLabels) || xAxisLabels.length === 0) return;
// 确保图表已完全渲染,检查容器尺寸
const chartDom = this.$refs.chartRef;
if (!chartDom || chartDom.offsetWidth === 0 || chartDom.offsetHeight === 0) {
console.warn('图表容器尺寸无效,跳过绘制类目边界线');
return;
}
const yMinPx = this.myChart.convertToPixel({ yAxisIndex: 0 }, chartConfig.yAxis.min);
const yMaxPx = this.myChart.convertToPixel({ yAxisIndex: 0 }, chartConfig.yAxis.max);
if (yMinPx === null || yMaxPx === null || isNaN(yMinPx) || isNaN(yMaxPx)) {
console.warn('convertToPixel 返回无效值,跳过绘制类目边界线');
return;
}
const yTopPx = Math.min(yMinPx, yMaxPx);
const yBottomPx = Math.max(yMinPx, yMaxPx);
// 只在上方画短竖线:长度占绘图区高度的 6%,并限制在 15-60 像素
const plotHeight = Math.abs(yBottomPx - yTopPx);
const stemLen = Math.max(15, Math.min(60, plotHeight * 0.06));
// 仅保留上方的线:整段都在 top 之上
const extraHead = Math.max(6, Math.min(30, plotHeight * 0.03));
const yStemStart = yTopPx - (extraHead + stemLen);
const yStemEnd = yTopPx - 1;
const graphics = [];
const n = xAxisLabels.length;
// 计算每个类目的像素中心
const centers = [];
for (let i = 0; i < n; i++) {
const cx = this.myChart.convertToPixel({ xAxisIndex: 0 }, i);
if (cx != null) centers.push(cx);
}
if (centers.length > 0) {
// 第一个点左侧边界:用前两个中心的半步估计
let firstEdge;
if (centers.length >= 2) {
firstEdge = centers[0] - (centers[1] - centers[0]) / 2;
} else {
// 只有一个点时,给一个合理的固定偏移
firstEdge = centers[0] - 40;
}
graphics.push({
type: 'line',
shape: { x1: firstEdge, y1: yStemStart, x2: firstEdge, y2: yStemEnd },
style: { stroke: '#bbb', lineWidth: 1.5 },
silent: true,
z: 120,
zlevel: 10
});
// 相邻两点之间的中点
for (let i = 0; i < centers.length - 1; i++) {
const mid = (centers[i] + centers[i + 1]) / 2;
graphics.push({
type: 'line',
shape: { x1: mid, y1: yStemStart, x2: mid, y2: yStemEnd },
style: { stroke: '#bbb', lineWidth: 1.5 },
silent: true,
z: 120,
zlevel: 10
});
}
// 右侧最后一个边界线:对称估算半步并绘制(补齐“最后那条线”)
let lastEdge;
if (centers.length >= 2) {
lastEdge = centers[centers.length - 1] + (centers[centers.length - 1] - centers[centers.length - 2]) / 2;
} else {
lastEdge = centers[0] + 40;
}
graphics.push({
type: 'line',
shape: { x1: lastEdge, y1: yStemStart, x2: lastEdge, y2: yStemEnd },
style: { stroke: '#bbb', lineWidth: 1.5 },
silent: true,
z: 120,
zlevel: 10
});
}
const prev = Array.isArray(this.currentGraphicElements) ? this.currentGraphicElements : [];
const merged = prev.concat(graphics);
this.currentGraphicElements = merged;
this.$nextTick(() => {
if (this.myChart) {
this.myChart.setOption({ graphic: { elements: merged } }, { replaceMerge: ['graphic'] });
}
});
},
// 图内根据 x 和 y2 渲染 SVG 纹理(每段使用该 x 在 stackedAreas 中最浅层 y2 对应的 svg),从顶部填充到 y2 深度
async drawTopBandSvg(xAxisLabels, chartConfig, stackedAreas) {
if (!this.myChart || !Array.isArray(xAxisLabels) || !stackedAreas) return;
// 确保图表已完全渲染,检查容器尺寸
const chartDom = this.$refs.chartRef;
if (!chartDom || chartDom.offsetWidth === 0 || chartDom.offsetHeight === 0) {
console.warn('图表容器尺寸无效,跳过绘制顶部SVG纹理');
return;
}
// 计算每个类目的像素中心
const centers = [];
for (let i = 0; i < xAxisLabels.length; i++) {
const cx = this.myChart.convertToPixel({ xAxisIndex: 0 }, i);
if (cx != null) centers.push(cx);
}
if (centers.length === 0) return;
// 计算段边界(第一个左边界 + 各中点)
let firstEdge;
if (centers.length >= 2) {
firstEdge = centers[0] - (centers[1] - centers[0]) / 2;
} else {
firstEdge = centers[0] - 40;
}
const boundaries = [firstEdge];
for (let i = 0; i < centers.length - 1; i++) {
boundaries.push((centers[i] + centers[i + 1]) / 2);
}
// 图内纵向像素范围:从图内顶部到 y2 所在像素
const yMinPx = this.myChart.convertToPixel({ yAxisIndex: 0 }, chartConfig.yAxis.min);
const yMaxPx = this.myChart.convertToPixel({ yAxisIndex: 0 }, chartConfig.yAxis.max);
if (yMinPx == null || yMaxPx == null) return;
const yTopPx = Math.min(yMinPx, yMaxPx);
const plotHeight = Math.abs(yMaxPx - yMinPx);
// 摊平 stackedAreas 以便按 x 查找图层(保留全部层,用于全部渲染)
const allLayers = [];
if (Array.isArray(stackedAreas)) {
for (const l of stackedAreas) if (l) allLayers.push(l);
} else {
for (const layers of Object.values(stackedAreas)) {
if (Array.isArray(layers)) allLayers.push(...layers.filter(Boolean));
}
}
const graphics = [];
// 仅在每口井的最后一列标注层名称
const depthIntervals = this.mockData?.wellData?.depthIntervals || [];
const lastIndexByJh = new Map();
for (let idx = 0; idx < depthIntervals.length; idx++) {
const jhVal = depthIntervals[idx]?.jh;
if (jhVal != null) lastIndexByJh.set(jhVal, idx);
}
for (let i = 0; i < xAxisLabels.length; i++) {
const xLabel = String(xAxisLabels[i]);
const left = boundaries[i];
const right = (i < boundaries.length - 1) ? boundaries[i + 1] : left + (centers[1] ? (centers[1] - centers[0]) : 80);
const width = Math.max(10, right - left);
// 找到包含该 x 的所有层,提取该 x 对应点的 y2;若有重叠(相同 y2)则只保留一次
const y2ToLayer = new Map();
for (const layer of allLayers) {
const pts = Array.isArray(layer?.points) ? layer.points : [];
const p = pts.find(p0 => String(p0?.x) === xLabel);
if (!p) continue;
const y2val = Number(p?.y2 ?? layer?.sjdjsd);
if (Number.isNaN(y2val)) continue;
if (!y2ToLayer.has(y2val)) y2ToLayer.set(y2val, layer);
}
const sortedY2 = Array.from(y2ToLayer.keys()).sort((a, b) => a - b);
let prevBottom = null; // 上一个片段的底(y2),为空则从图内顶部开始
for (const y2val of sortedY2) {
const layer = y2ToLayer.get(y2val);
const yTopDepth = prevBottom == null ? chartConfig?.yAxis?.min : prevBottom;
const yBottomDepth = y2val;
const yPixTop = this.myChart.convertToPixel({ yAxisIndex: 0 }, yTopDepth);
const yPixBottom = this.myChart.convertToPixel({ yAxisIndex: 0 }, yBottomDepth);
if (yPixTop == null || yPixBottom == null) {
prevBottom = y2val;
continue;
}
const rectTop = Math.min(yPixTop, yPixBottom);
const rectBottom = Math.max(yPixTop, yPixBottom);
const y1pix = Math.max(yTopPx, rectTop);
const y2pix = Math.min(Math.max(yMinPx, yMaxPx), rectBottom);
const height = Math.max(0, y2pix - y1pix);
if (height > 0) {
let fill = '#d0d3d8';
if (layer?.svg) {
try {
const img = await this.createSvgImage(layer.svg);
fill = { image: img, repeat: 'repeat' };
} catch (e) { /* ignore */
}
}
graphics.push({
type: 'rect',
shape: { x: left, y: y1pix, width, height },
style: { fill, stroke: 'rgba(0,0,0,0.12)', lineWidth: 1 },
silent: true,
z: -10,
zlevel: 1,
__band: true
});
}
prevBottom = y2val;
}
}
// 合并到当前 graphic 上:先移除旧 band,再追加新的,避免重复渲染
const prev = Array.isArray(this.currentGraphicElements) ? this.currentGraphicElements : [];
const kept = prev.filter(el => !el.__band);
const merged = kept.concat(graphics);
this.currentGraphicElements = merged;
this.$nextTick(() => {
if (this.myChart) {
this.myChart.setOption({ graphic: { elements: merged } }, { replaceMerge: ['graphic'] });
}
});
},
// 导出图表为图片
exportChart() {
if (!this.myChart) {
this.$message.warning('图表尚未加载完成,请稍候再试');
return;
}
try {
// 获取图表图片数据
const dataURL = this.myChart.getDataURL({
type: 'png',
pixelRatio: 2, // 提高图片清晰度
backgroundColor: '#fff'
});
// 创建下载链接
const link = document.createElement('a');
const fileName = this.jh ? `直方图_${this.jh}_${new Date().getTime()}.png` : `直方图_${new Date().getTime()}.png`;
link.href = dataURL;
link.download = fileName;
link.style.display = 'none';
// 触发下载
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
this.$message.success('图表导出成功');
} catch (error) {
console.error('导出图表失败:', error);
this.$message.error('导出图表失败,请稍候再试');
}
}
},
};
</script>
<style lang="scss" scoped>
/* 容器样式优化 */
.chart-container {
width: 100%;
min-height: calc(100vh - 140px);
padding: 0 10px;
margin: 0;
box-sizing: border-box;
display: flex;
flex-direction: column;
position: relative;
overflow: auto;
}
.chart-layout {
display: flex;
flex: 1;
width: 100%;
gap: 5px;
align-items: stretch;
justify-content: flex-start;
overflow: hidden;
}
.strata-legend {
width: 240px;
min-width: 240px;
max-height: 100%;
padding: 16px;
border-radius: 16px;
background: #fff;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08);
border: 1px solid rgba(15, 23, 42, 0.06);
display: flex;
flex-direction: column;
gap: 12px;
}
.legend-header {
font-size: 16px;
font-weight: 600;
color: #1f2937;
border-bottom: 1px solid rgba(15, 23, 42, 0.08);
padding-bottom: 8px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.legend-list {
overflow-y: auto;
flex: 1;
display: flex;
flex-direction: column;
gap: 1px;
padding-right: 4px;
}
.legend-item {
display: flex;
align-items: center;
gap: 5px;
padding: 0px 4px;
border-radius: 8px;
transition: background 0.2s ease;
}
.legend-item:hover {
background-color: rgba(59, 130, 246, 0.08);
}
.legend-icon {
width: 40px;
height: 24px;
border-radius: 6px;
border: 1px solid rgba(15, 23, 42, 0.15);
background-color: #d1d5db;
background-size: cover !important;
}
.legend-label {
flex: 1;
font-size: 13px;
font-weight: 500;
color: #1f2937;
}
/* 井号显示样式 */
.well-number-display {
position: absolute;
top: 16px;
left: 16px;
z-index: 5;
background: transparent;
padding: 8px 12px;
display: flex;
align-items: center;
gap: 6px;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
transition: all 0.3s ease;
opacity: 0.9;
}
/* 导出按钮样式 */
::v-deep .export-btn {
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%) !important;
color: #fff !important;
border: none !important;
border-radius: 6px !important;
padding: 6px 14px !important;
font-size: 13px !important;
font-weight: 500 !important;
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3) !important;
transition: all 0.3s ease !important;
}
::v-deep .export-btn:hover:not(:disabled) {
background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%) !important;
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.4) !important;
transform: translateY(-1px) !important;
}
::v-deep .export-btn:active:not(:disabled) {
transform: translateY(0) !important;
box-shadow: 0 2px 6px rgba(59, 130, 246, 0.3) !important;
}
::v-deep .export-btn:disabled {
background: #9ca3af !important;
cursor: not-allowed !important;
opacity: 0.6 !important;
box-shadow: none !important;
}
::v-deep .export-btn .el-icon-download {
color: #fff !important;
}
.well-number-display:hover {
opacity: 1;
}
.well-label {
color: #6b7280;
font-size: 13px;
font-weight: 500;
white-space: nowrap;
}
.well-number {
color: #3b82f6;
font-size: 14px;
font-weight: 600;
}
/* 图表容器样式 */
.chart {
flex: 1;
width: 100%;
max-width: 100%;
height: 100%;
min-height: 400px;
display: block;
border-radius: 16px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
border: 1px solid rgba(255, 255, 255, 0.8);
backdrop-filter: blur(10px);
transition: all 0.3s ease;
}
.chart:hover {
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.15);
/* transform: translateY(-2px); */
}
/* 加载状态样式 */
.loading-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.95);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 10;
border-radius: 16px;
backdrop-filter: blur(10px);
}
.loading-spinner {
width: 48px;
height: 48px;
border: 4px solid #f3f4f6;
border-top: 4px solid #3b82f6;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 16px;
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.2);
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
/* 响应式优化 */
@media (max-width: 768px) {
.chart-container {
padding: 0 10px;
}
.chart {
min-height: 300px;
border-radius: 12px;
}
.export-btn {
padding: 4px 8px;
font-size: 12px;
}
}
/* 深色模式支持 */
@media (prefers-color-scheme: dark) {
.chart {
/* background: linear-gradient(135deg, #1f2937 0%, #111827 100%); */
/* box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4); */
border: 1px solid rgba(255, 255, 255, 0.1);
}
/*
.chart:hover {
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.5);
} */
.loading-overlay {
background: rgba(17, 24, 39, 0.95);
}
}
/* 动画效果 */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.chart-container {
animation: fadeIn 0.6s ease-out;
}
</style>
<template>
<div class="chart-container">
<div class="chart-layout">
<div id="mainzftdj" class="chart" ref="chartRef"></div>
<aside v-if="legendItems && legendItems.length" class="strata-legend">
<div class="legend-header">
<span>层位图例</span>
<el-button class="export-btn" @click="exportChart" :disabled="loading || !myChart" title="导出图表为图片"
icon="el-icon-download" size="small">
导出
</el-button>
</div>
<div class="legend-list">
<div v-for="(item, index) in legendItems" :key="item.name || index" class="legend-item">
<div class="legend-icon" :style="getLegendSwatchStyle(item)"></div>
<span class="legend-label">{{ item.name || '-' }}</span>
</div>
</div>
</aside>
</div>
<div v-if="loading" class="loading-overlay">
<div class="loading-spinner"></div>
<span>加载中...</span>
</div>
</div>
</template>
<script>
import * as echarts from "echarts";
import { getdjZft } from "@/api/optimization/initialization";
import { getljqxData } from "@/api/system/cjsjLas";
import { listLjSssjSd } from "@/api/optimization/ljSssjSd";
import { text } from "d3";
export default {
name: "HistogramGraph",
props: {
jh: {
type: String,
default: "",
},
jhs: {
type: String,
default: ''
},
// 美化配置选项
theme: {
type: String,
default: "modern", // modern, elegant, vibrant
validator: value => ["modern", "elegant", "vibrant"].includes(value)
}
},
data() {
return {
mockData: {},
legendItems: [],
myChart: null,
initRetryCount: 0,
maxRetryCount: 5,
resizeObserver: null,
loading: false,
debounceTimer: null,
lastStackedAreas: null,
lastChartConfig: null,
lastXAxisLabels: null,
lastDepthIntervals: null,
currentGraphicElements: [],
// 录井多曲线数据(钻时/扭矩/立压/钻压/转速/泵冲/入口流量)
curveData: null,
};
},
computed: {
// 根据主题获取颜色配置
colorScheme() {
const schemes = {
modern: {
primary: "#3B82F6",
secondary: "#10B981",
accent: "#F59E0B",
background: "#F8FAFC",
text: "#1F2937",
border: "#E5E7EB",
gradient: {
start: "#3B82F6",
end: "#1D4ED8"
}
},
elegant: {
primary: "#6366F1",
secondary: "#8B5CF6",
accent: "#EC4899",
background: "#FAFAFA",
text: "#374151",
border: "#D1D5DB",
gradient: {
start: "#6366F1",
end: "#4F46E5"
}
},
vibrant: {
primary: "#EF4444",
secondary: "#06B6D4",
accent: "#F97316",
background: "#FEFEFE",
text: "#111827",
border: "#F3F4F6",
gradient: {
start: "#EF4444",
end: "#DC2626"
}
}
};
return schemes[this.theme];
},
legendPanelWidth() {
return this.legendItems && this.legendItems.length ? 260 : 0;
}
},
watch: {
jh: {
handler() {
// 不自动刷新,等待父级触发 loadData
},
immediate: false
},
jhs: {
handler() {
// jhs 变化时等待父级触发 loadData
},
immediate: false
},
legendItems: {
handler() {
this.$nextTick(() => {
const chartDom = this.$refs.chartRef;
if (chartDom) {
this.setChartDimensions(chartDom);
this.performResize();
}
});
},
deep: true
}
},
mounted() {
// 初始化空图表,不拉数据;等待父组件触发 loadData
this.initChart();
this.setupEventListeners();
},
beforeDestroy() {
this.cleanup();
},
methods: {
// 初始化图表
async initChart() {
try {
this.loading = true;
const chartDom = this.$refs.chartRef;
if (!chartDom) {
throw new Error("未找到图表容器 DOM");
}
this.setChartDimensions(chartDom);
// 重试机制优化
if (this.shouldRetry()) {
this.scheduleRetry();
return;
}
this.resetRetryCount();
this.disposeChart();
this.createChart(chartDom);
// 并行获取直方图数据 + 多曲线数据
const [mockData, curveData] = await Promise.all([
this.getList(),
this.getCurveData()
]);
this.curveData = curveData;
if (!this.hasValidData(mockData)) {
this.renderEmpty(chartDom);
return;
}
await this.renderRealData(mockData);
} catch (error) {
console.error("图表初始化失败:", error);
this.handleError(error);
} finally {
this.loading = false;
}
},
// 设置图表尺寸
setChartDimensions(chartDom) {
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
const containerWidth = this.$el?.clientWidth || viewportWidth;
const rect = this.$el?.getBoundingClientRect();
const topOffset = rect ? rect.top : 0;
const legendWidth = this.legendPanelWidth || 0;
const containerPadding = 20; // chart-container 水平 padding 之和
const panelGap = legendWidth ? 5 : 0; // 与右侧图例的间距
const safetyMargin = 12; // 额外预留
const widthPadding = containerPadding + panelGap + legendWidth + safetyMargin;
const availableWidth = Math.max(360, containerWidth - widthPadding);
const verticalPadding = 40;
const heightByViewport = viewportHeight - topOffset - verticalPadding;
const fallbackHeight = this.$el?.clientHeight || viewportHeight;
const availableHeight = Math.max(360, heightByViewport, fallbackHeight - 20);
chartDom.style.width = `${availableWidth}px`;
chartDom.style.height = `${availableHeight}px`;
chartDom.offsetHeight; // 强制重排
},
// 检查是否需要重试
shouldRetry() {
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
const availableHeight = viewportHeight - 120;
const availableWidth = viewportWidth - 80;
return (availableWidth <= 0 || availableHeight <= 0) && this.initRetryCount < this.maxRetryCount;
},
// 安排重试
scheduleRetry() {
this.initRetryCount++;
setTimeout(() => this.initChart(), 500);
},
// 重置重试计数
resetRetryCount() {
this.initRetryCount = 0;
},
// 销毁图表
disposeChart() {
if (this.myChart) {
this.myChart.dispose();
}
},
// 创建图表实例
createChart(chartDom) {
this.myChart = echarts.init(chartDom, null, {
renderer: 'canvas',
useDirtyRect: true
});
},
// 检查数据有效性
hasValidData(mockData) {
return mockData &&
mockData.wellData &&
mockData.wellData.depthLine &&
mockData.wellData.depthLine.length > 0;
},
// 设置事件监听器
setupEventListeners() {
window.addEventListener("resize", this.handleResize);
this.observeParentResize();
},
// 清理资源
cleanup() {
window.removeEventListener("resize", this.handleResize);
if (this.myChart) {
this.myChart.dispose();
}
if (this.resizeObserver) {
this.resizeObserver.disconnect();
}
if (this.debounceTimer) {
clearTimeout(this.debounceTimer);
}
},
// 观察父容器大小变化
observeParentResize() {
const parentDom = this.$refs.chartRef?.parentElement;
if (parentDom && window.ResizeObserver) {
this.resizeObserver = new ResizeObserver(() => {
this.debouncedResize();
});
this.resizeObserver.observe(parentDom);
}
},
// 获取数据:以 jhs 为主,根据井号在相同区域展示
async getList() {
try {
// 以 jhs 为数据源:jhs 存在时用 jhs,否则用 jh
const jhsParam = this.jhs && String(this.jhs).trim()
? String(this.jhs).trim()
: (this.jh ? String(this.jh).trim() : '');
if (!jhsParam) {
this.mockData = {};
this.legendItems = [];
return this.mockData;
}
const res = await getdjZft({ jhs: jhsParam });
let mockData = res?.mockData || {};
// 按区域(HT)分组,相同区域的井号在一起展示
if (mockData.wellData) {
mockData = this.reorderWellDataByRegion(mockData);
}
this.mockData = mockData;
const legendList = Array.isArray(res?.tlList)
? res.tlList
: Array.isArray(res?.mockData?.tlList)
? res.mockData.tlList
: [];
this.legendItems = legendList;
return this.mockData;
} catch (error) {
console.error("获取数据失败:", error);
throw error;
}
},
// 按区域(x/HT)分组重排:相同区域的井号相邻展示
reorderWellDataByRegion(mockData) {
const wellData = mockData.wellData || {};
const depthIntervals = wellData.depthIntervals || [];
const depthLine = wellData.depthLine || [];
const stackedAreas = wellData.stackedAreas || {};
if (depthIntervals.length === 0 || depthLine.length === 0) return mockData;
// 获取 jhs 顺序,用于同区域内井号排序
const jhsOrder = [];
const jhsStr = this.jhs || this.jh || '';
if (jhsStr) {
jhsOrder.push(...String(jhsStr).split(',').map(s => s.trim()).filter(Boolean));
}
const jhOrderMap = new Map();
jhsOrder.forEach((jh, idx) => { jhOrderMap.set(jh, idx); });
// 按区域(x)分组,同区域内按 jhs 顺序排
const byRegion = new Map();
depthIntervals.forEach((di, idx) => {
const region = di?.x ?? di?.ht ?? String(idx);
if (!byRegion.has(region)) byRegion.set(region, []);
byRegion.get(region).push({ di, depthLinePoint: depthLine[idx], origIdx: idx });
});
// 区域排序:保持首次出现顺序
const regionOrder = Array.from(byRegion.keys());
regionOrder.sort((a, b) => {
const aMin = Math.min(...byRegion.get(a).map(({ origIdx }) => origIdx));
const bMin = Math.min(...byRegion.get(b).map(({ origIdx }) => origIdx));
return aMin - bMin;
});
const newDepthIntervals = [];
const newDepthLine = [];
for (const region of regionOrder) {
const items = byRegion.get(region);
items.sort((a, b) => {
const orderA = jhOrderMap.has(a.di.jh) ? jhOrderMap.get(a.di.jh) : 9999;
const orderB = jhOrderMap.has(b.di.jh) ? jhOrderMap.get(b.di.jh) : 9999;
return orderA - orderB;
});
items.forEach(({ di, depthLinePoint }) => {
newDepthIntervals.push(di);
newDepthLine.push(depthLinePoint || { x: di.x, y: 0 });
});
}
return {
...mockData,
wellData: {
...wellData,
depthIntervals: newDepthIntervals,
depthLine: newDepthLine,
stackedAreas
}
};
},
// 获取当前主井的录井多曲线数据(钻时等),用于在同一张图上叠加折线
async getCurveData() {
// 优先用 props.jh;如果没有,再从 jhs 里取第一个井号做回退
let targetJh = this.jh;
if (!targetJh && this.jhs) {
const arr = String(this.jhs)
.split(",")
.map((s) => s.trim())
.filter((s) => s);
if (arr.length) {
targetJh = arr[0];
}
}
console.log("HistogramGraph.getCurveData => jh:", targetJh, "props:", {
jh: this.jh,
jhs: this.jhs,
});
if (!targetJh) {
console.warn("HistogramGraph.getCurveData: 无有效井号,跳过获取曲线数据");
return null;
}
try {
const [ljqxRes, ljSssjRes] = await Promise.all([
getljqxData({ jh: targetJh }),
listLjSssjSd({ jh: targetJh, pageNum: 1, pageSize: 999999 }),
]);
console.log("HistogramGraph.getCurveData 接口返回:", {
ljqxRes,
ljSssjRes,
});
const processed = this.processCurveData(ljqxRes, ljSssjRes);
console.log("HistogramGraph.getCurveData 处理后曲线数据:", processed);
return processed;
} catch (e) {
console.error("获取录井多曲线数据失败:", e);
return null;
}
},
// 处理录井多曲线数据,结构参考 DrillingTimeChart.vue
processCurveData(ljqxRes, ljSssjRes) {
console.log("HistogramGraph.processCurveData 输入原始数据:", {
ljqxRes,
ljSssjRes,
});
const processArrayData = (dataList, fieldName) => {
if (!dataList || !Array.isArray(dataList) || dataList.length === 0) {
return [];
}
// 对象数组 [{dept: xxx, value: xxx}, ...]
if (typeof dataList[0] === "object" && dataList[0].hasOwnProperty("dept")) {
return dataList.map((item) => ({
depth: item.dept || item.depth,
value: item[fieldName] || item.value || item,
}));
}
// 普通数组,深度从录井整米数据里取
return dataList.map((value, index) => {
const depth =
ljSssjRes && ljSssjRes.rows && ljSssjRes.rows[index]
? ljSssjRes.rows[index].js
: null;
return { depth, value };
});
};
// 钻时:录井整米数据 rows.zs
let drillingTimeData = [];
if (ljSssjRes && ljSssjRes.rows && Array.isArray(ljSssjRes.rows)) {
drillingTimeData = ljSssjRes.rows.map((item) => ({
depth: item.js,
value: item.zs,
}));
}
const torqueData = processArrayData(ljqxRes.njList, "nj");
const standpipePressureData = processArrayData(ljqxRes.lyList, "ly");
const drillingPressureData = processArrayData(ljqxRes.zyList, "zy");
const rpmData = processArrayData(ljqxRes.zs1List, "zs1");
const rkllData = processArrayData(ljqxRes.rkllList, "rkll");
// 泵冲:录井整米数据 rows.zbc
let pumpStrokeData = [];
if (ljSssjRes && ljSssjRes.rows && Array.isArray(ljSssjRes.rows)) {
pumpStrokeData = ljSssjRes.rows.map((item) => ({
depth: item.js,
value: item.zbc,
}));
}
const result = {
drillingTime: drillingTimeData,
torque: torqueData,
standpipePressure: standpipePressureData,
drillingPressure: drillingPressureData,
rpm: rpmData,
pumpStroke: pumpStrokeData,
rkllData: rkllData,
};
console.log("HistogramGraph.processCurveData 输出:", {
drillingTimeLen: drillingTimeData.length,
torqueLen: torqueData.length,
standpipePressureLen: standpipePressureData.length,
drillingPressureLen: drillingPressureData.length,
rpmLen: rpmData.length,
pumpStrokeLen: pumpStrokeData.length,
rkllLen: rkllData.length,
result,
});
return result;
},
// 供父组件在切换到本tab时调用
async loadData() {
await this.initChart();
},
// 处理窗口大小变化
handleResize() {
this.debouncedResize();
},
// 防抖处理resize
debouncedResize() {
if (this.debounceTimer) {
clearTimeout(this.debounceTimer);
}
this.debounceTimer = setTimeout(() => {
this.performResize();
}, 200);
},
// 执行resize操作
performResize() {
if (this.myChart) {
const chartDom = this.$refs.chartRef;
if (chartDom) {
this.setChartDimensions(chartDom);
}
this.$nextTick(() => {
if (this.myChart) {
this.myChart.resize();
if (this.lastStackedAreas && this.lastChartConfig && this.lastXAxisLabels) {
this.drawStratumLabels(this.lastStackedAreas, this.lastChartConfig, this.lastXAxisLabels);
}
if (this.lastDepthIntervals && this.lastXAxisLabels && this.lastChartConfig) {
this.drawJhSeparators(this.lastDepthIntervals, this.lastXAxisLabels, this.lastChartConfig);
}
if (this.lastXAxisLabels && this.lastChartConfig) {
this.drawCategoryEdgeLines(this.lastXAxisLabels, this.lastChartConfig);
this.drawTopBandSvg(this.lastXAxisLabels, this.lastChartConfig, this.lastStackedAreas);
}
}
});
}
},
// 刷新图表
async refreshChart() {
if (this.myChart) {
await this.initChart();
}
},
// 渲染真实数据
async renderRealData(mockData) {
try {
const { wellData } = mockData;
console.log(wellData, 'wellData');
if (this.curveData) {
console.log("HistogramGraph.renderRealData 当前曲线数据:", this.curveData);
} else {
console.warn("HistogramGraph.renderRealData: 当前 curveData 为空或未获取到");
}
const xAxisLabels = wellData.depthLine.map((point) => point.x);
try {
window.__hist_xLabels = xAxisLabels;
} catch (e) {
}
const option = {
...this.getDefaultChartOption(),
xAxis: this.createXAxis(xAxisLabels),
yAxis: this.createYAxis(mockData.chartConfig),
series: this.createSeries(wellData)
};
// 如果有录井多曲线数据,则在同一 grid 中叠加一组使用 value 型 x 轴的折线
if (this.curveData) {
this.extendWithCurves(option, mockData.chartConfig);
}
const chartDom = this.$refs.chartRef;
if (chartDom) {
this.setChartDimensions(chartDom);
}
this.myChart.setOption(option, true);
// 确保图表完全渲染后再resize和绘制
this.$nextTick(() => {
// 先resize确保图表尺寸正确
this.myChart.resize();
// 使用 requestAnimationFrame 确保浏览器完成渲染后再绘制
requestAnimationFrame(() => {
requestAnimationFrame(() => {
// 再次确认尺寸,确保图表已完全渲染
const chartDom = this.$refs.chartRef;
if (chartDom && chartDom.offsetWidth > 0 && chartDom.offsetHeight > 0) {
// 再次resize确保尺寸准确
this.myChart.resize();
// 保存数据用于后续resize时重新绘制
this.lastStackedAreas = wellData.stackedAreas;
this.lastChartConfig = mockData.chartConfig;
this.lastXAxisLabels = xAxisLabels;
this.lastDepthIntervals = wellData.depthIntervals || [];
// 绘制所有图形元素
this.drawStratumLabels(this.lastStackedAreas, this.lastChartConfig, this.lastXAxisLabels);
this.drawJhSeparators(this.lastDepthIntervals, this.lastXAxisLabels, this.lastChartConfig);
this.drawCategoryEdgeLines(this.lastXAxisLabels, this.lastChartConfig);
this.drawTopBandSvg(this.lastXAxisLabels, this.lastChartConfig, this.lastStackedAreas);
} else {
// 如果尺寸还未确定,延迟重试
setTimeout(() => {
if (this.myChart) {
this.myChart.resize();
this.lastStackedAreas = wellData.stackedAreas;
this.lastChartConfig = mockData.chartConfig;
this.lastXAxisLabels = xAxisLabels;
this.lastDepthIntervals = wellData.depthIntervals || [];
this.drawStratumLabels(this.lastStackedAreas, this.lastChartConfig, this.lastXAxisLabels);
this.drawJhSeparators(this.lastDepthIntervals, this.lastXAxisLabels, this.lastChartConfig);
this.drawCategoryEdgeLines(this.lastXAxisLabels, this.lastChartConfig);
this.drawTopBandSvg(this.lastXAxisLabels, this.lastChartConfig, this.lastStackedAreas);
}
}, 150);
}
});
});
});
} catch (error) {
console.error("渲染数据失败:", error);
this.handleError(error);
}
},
// 获取默认图表配置
getDefaultChartOption() {
const colors = this.colorScheme;
return {
title: { text: "", subtext: "" },
aria: { enabled: false },
backgroundColor: colors.background,
tooltip: {
trigger: "axis",
axisPointer: {
type: "cross",
label: { backgroundColor: colors.primary, show: false },
crossStyle: { color: colors.border }
},
backgroundColor: 'rgba(255,255,255,0.95)',
borderColor: colors.border,
borderWidth: 1,
textStyle: { color: colors.text },
extraCssText: 'box-shadow: 0 4px 12px rgba(0,0,0,0.15); border-radius: 8px;',
formatter: (params) => {
if (!params || params.length === 0) return '';
const dataIndex = params[0].dataIndex;
const depthInterval = this.mockData?.wellData?.depthIntervals?.[dataIndex];
if (!depthInterval) {
// 如果没有depthInterval数据,使用默认格式
let result = `<div style="font-weight: 600; margin-bottom: 8px; color: ${colors.primary};">${params[0].axisValue}</div>`;
params.forEach(param => {
const color = param.color;
result += `<div style="margin: 4px 0;">
<span style="display: inline-block; width: 10px; height: 10px; background-color: ${color}; border-radius: 50%; margin-right: 8px;"></span>
<span style="font-weight: 500;">${param.seriesName}:</span>
<span style="margin-left: 8px; font-weight: 600;">${param.value}</span>
</div>`;
});
return result;
}
// 显示井号信息
let result = `<div style="font-weight: 600; margin-bottom: 8px; color: ${colors.primary};">${depthInterval.jh || depthInterval.x}</div>`;
// 显示详细信息
const modelLabel = this.formatXAxisLabel(depthInterval.x || '-');
result += `<div style="margin: 4px 0; padding: 4px 0; border-top: 1px solid ${colors.border};">
<div style="margin: 2px 0;"><span style="font-weight: 500;">型号:</span> <span style="margin-left: 8px; font-weight: 600;">${modelLabel}</span></div>
<div style="margin: 2px 0;"><span style="font-weight: 500;">尺寸:</span> <span style="margin-left: 8px; font-weight: 600;">${depthInterval.ztcc || '-'}</span></div>
<div style="margin: 2px 0;"><span style="font-weight: 500;">进尺mm:</span> <span style="margin-left: 8px; font-weight: 600;">${depthInterval.jc || '-'}</span></div>
<div style="margin: 2px 0;"><span style="font-weight: 500;">深度区间:</span> <span style="margin-left: 8px; font-weight: 600;">${depthInterval.interval || '-'}</span></div>
</div>`;
// 只显示非地层信息的系列数据(过滤掉地层相关的系列)
// params.forEach(param => {
// // 过滤掉地层相关的系列名称
// if (param.seriesName && !param.seriesName.includes('地层') && param.seriesName !== '占位') {
// const color = param.color;
// result += `<div style="margin: 4px 0;">
// <span style="display: inline-block; width: 10px; height: 10px; background-color: ${color}; border-radius: 50%; margin-right: 8px;"></span>
// <span style="font-weight: 500;">${param.seriesName}:</span>
// <span style="margin-left: 8px; font-weight: 600;">${param.value}</span>
// </div>`;
// }
// });
return result;
}
},
grid: {
top: 20,
left: "2%",
right: "3%",
bottom: "10%",
containLabel: true,
show: false,
},
animation: true,
animationDuration: 1500,
animationEasing: 'cubicOut',
animationDelay: function (idx) {
return idx * 100;
}
};
},
// 处理区域系列
async processAreaSeries(wellData, xAxisLabels) {
const stackedAreas = wellData?.stackedAreas || [];
// 兼容旧格式:数组(所有井已按全局顺序平铺)
if (Array.isArray(stackedAreas)) {
return await Promise.all(
stackedAreas
.filter(area => area.svg !== null)
.map(async (area) => {
let areaStyle = { opacity: 0.6 };
if (area.svg) {
try {
const svgImage = await this.createSvgImage(area.svg);
areaStyle.color = { image: svgImage, repeat: "repeat" };
} catch (error) {
console.error('SVG转换失败:', error);
}
}
return {
name: area.name,
type: "line",
xAxisIndex: 1,
z: -1,
stack: "总量",
symbol: "none",
showSymbol: false,
areaStyle: areaStyle,
lineStyle: { width: 0 },
data: area.points.map((point) => (point && point.y2 != null) ? point.y2 : point.y),
};
})
);
}
// 新格式:对象,key 为井号,每口井内是该井的地层数组
const totalLen = Array.isArray(xAxisLabels) ? xAxisLabels.length : 0;
console.log(totalLen, 'totalLen');
// 全局 x 标签 -> 索引数组 映射
const xToIndices = new Map();
console.log(xToIndices, 'xToIndices');
(xAxisLabels || []).forEach((x, idx) => {
const key = String(x);
if (!xToIndices.has(key)) xToIndices.set(key, []);
xToIndices.get(key).push(idx);
});
const buildNullArray = (len) => Array.from({ length: len }, () => null);
console.log(buildNullArray, 'buildNullArray');
const results = [];
for (const [jh, layers] of Object.entries(stackedAreas)) {
if (!Array.isArray(layers) || layers.length === 0) continue;
// 估算该井的横向覆盖范围:取该井所有层的所有点对应的全局索引的 min/max
const indicesForJh = [];
const missingInXAxis = [];
for (const area of layers) {
for (const p of (area?.points || [])) {
const arr = xToIndices.get(String(p?.x)) || [];
if (arr.length === 0) {
missingInXAxis.push({ x: p?.x, jh, areaName: area?.name });
} else {
indicesForJh.push(...arr);
}
}
}
const startIdx = Math.min(...indicesForJh);
const endIdx = Math.max(...indicesForJh);
for (const area of (layers || [])) {
if (area == null) continue;
let areaStyle = { opacity: 0.6 };
if (area.svg) {
try {
const svgImage = await this.createSvgImage(area.svg);
areaStyle.color = { image: svgImage, repeat: "repeat" };
} catch (error) {
console.error('SVG转换失败:', error);
}
}
const data = buildNullArray(totalLen);
// 把该层每个点的 y2/y 填到该点 x 对应的全局索引中,仅限落在该井的覆盖范围内
const outOfRangePoints = [];
for (const p of (area.points || [])) {
const yVal = (p && p.y2 != null) ? p.y2 : p?.y;
const cands = xToIndices.get(String(p?.x)) || [];
if (cands.length === 0) continue;
let placed = false;
cands.forEach(ix => {
if (ix >= startIdx && ix <= endIdx) {
data[ix] = yVal;
placed = true;
}
});
if (!placed) {
outOfRangePoints.push({ x: p?.x, cands, jh, areaName: area?.name, startIdx, endIdx });
}
}
results.push({
name: area.name,
type: "line",
xAxisIndex: 1,
z: -1,
stack: "总量",
symbol: "none",
showSymbol: false,
areaStyle: areaStyle,
lineStyle: { width: 0 },
data
});
// 仅在有异常时输出调试信息,方便定位 x 匹配问题
if (missingInXAxis.length > 0 || outOfRangePoints.length > 0) {
// 使用 console.group 让日志更清晰
console.groupCollapsed && console.groupCollapsed(`层定位告警: 井号=${jh}`);
if (missingInXAxis.length > 0) {
console.warn('x 不在 xAxisLabels 中:', missingInXAxis);
}
if (outOfRangePoints.length > 0) {
console.warn('x 命中但不在井段范围内:', outOfRangePoints);
console.info('井段范围:', { jh, startIdx, endIdx });
}
console.groupEnd && console.groupEnd();
}
}
}
return results;
},
// 创建X轴配置
createXAxis(xAxisLabels) {
const colors = this.colorScheme;
const formatLabel = (value) => this.formatXAxisLabel(value);
return [
{
type: "category",
boundaryGap: true,
position: "top",
data: xAxisLabels,
axisLabel: {
interval: 0,
rotate: -90,
margin: 30,
align: "center",
fontSize: 12,
color: colors.text,
fontWeight: 500,
formatter: formatLabel
},
axisTick: {
alignWithLabel: true,
length: 6,
lineStyle: { color: colors.border }
},
splitLine: { show: false },
axisLine: {
show: true,
lineStyle: {
color: colors.border,
width: 2
}
}
},
{
type: "category",
boundaryGap: false,
position: "top",
show: false,
data: xAxisLabels
},
];
},
// 创建Y轴配置
createYAxis(chartConfig) {
const colors = this.colorScheme;
return [
{
type: "value",
name: "井深(m)",
nameTextStyle: {
color: colors.text,
fontSize: 13,
fontWeight: 600,
padding: [0, 0, 0, 8]
},
min: chartConfig.yAxis.min,
max: chartConfig.yAxis.max,
interval: chartConfig.yAxis.interval,
inverse: true,
axisLabel: {
formatter: "{value}",
fontSize: 12,
color: colors.text,
fontWeight: 500
},
splitLine: {
show: true,
lineStyle: {
color: colors.border,
type: 'dashed',
opacity: 0.3
}
},
axisLine: {
show: true,
lineStyle: {
color: colors.border,
width: 2
}
},
axisTick: {
show: true,
lineStyle: { color: colors.border }
}
},
{
type: "value",
name: "深度(m)",
nameTextStyle: {
color: colors.text,
fontSize: 13,
fontWeight: 600,
padding: [0, 8, 0, 55]
},
min: chartConfig.yAxis2.min,
max: chartConfig.yAxis2.max,
interval: chartConfig.yAxis2.interval,
position: "right",
axisLabel: {
formatter: "{value}",
fontSize: 12,
color: colors.text,
fontWeight: 500
},
splitLine: { show: false },
axisLine: {
show: true,
lineStyle: {
color: colors.border,
width: 2
}
},
axisTick: {
show: true,
lineStyle: { color: colors.border }
}
},
];
},
// 创建系列配置
createSeries(wellData, areaSeries) {
const colors = this.colorScheme;
return [
{
name: "井深数据",
type: "line",
zlevel: 25,
yAxisIndex: 1,
symbol: "circle",
symbolSize: 10,
itemStyle: {
color: colors.accent,
borderColor: '#fff',
borderWidth: 3,
shadowColor: 'rgba(0,0,0,0.2)',
shadowBlur: 8
},
lineStyle: {
color: colors.accent,
width: 3,
type: 'solid',
shadowColor: 'rgba(0,0,0,0.1)',
shadowBlur: 4
},
label: {
show: true,
position: "top",
formatter: "{c}",
fontSize: 13,
fontWeight: 600,
color: colors.accent,
backgroundColor: "rgba(255,255,255,0.95)",
padding: [6, 8],
borderRadius: 6,
borderColor: colors.accent,
borderWidth: 2,
shadowColor: 'rgba(0,0,0,0.1)',
shadowBlur: 4
},
data: wellData.depthLine.map((point) => point.y),
},
{
name: "占位",
type: "bar",
zlevel: 20,
stack: "total",
silent: true,
barWidth: "20%",
itemStyle: {
borderColor: "transparent",
color: "transparent"
},
emphasis: {
itemStyle: {
borderColor: "transparent",
color: "transparent"
}
},
data: wellData.depthIntervals.map((item) => item.placeholder),
},
{
name: "深度区间",
type: "bar",
zlevel: 20,
stack: "total",
barWidth: "15%",
label: {
show: true,
position: 'insideTop',
color: '#fff',
fontSize: 11,
fontWeight: 600,
backgroundColor: 'rgba(0,0,0,0.1)',
padding: [2, 3],
borderRadius: 4,
borderColor: 'rgba(255,255,255,0.3)',
borderWidth: 1,
formatter: (params) => {
const di = wellData.depthIntervals[params.dataIndex];
const value = di?.jc ?? di?.interval;
if (value === undefined || value === null || value === '') return params.data;
return `${value}`;
}
},
itemStyle: {
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{ offset: 0, color: colors.gradient.start },
{ offset: 1, color: colors.gradient.end }
]
},
borderRadius: [4, 4, 0, 0],
shadowColor: 'rgba(0,0,0,0.2)',
shadowBlur: 8,
shadowOffsetY: 2
},
emphasis: {
itemStyle: {
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{ offset: 0, color: this.adjustColor(colors.gradient.start, 1.2) },
{ offset: 1, color: this.adjustColor(colors.gradient.end, 1.2) }
]
},
shadowBlur: 12,
shadowOffsetY: 4
}
},
data: wellData.depthIntervals.map((item) => {
const value = item?.jc ?? item?.interval;
return typeof value === 'number' ? value : Number(value) || 0;
}),
},
// 底部显示 interval 数值的透明条,用于放置底部标签
{
name: '深度区间-底部标签',
type: 'bar',
zlevel: 20,
// 与主柱重叠以便把标签放在柱底部区域内
barGap: '-100%',
barWidth: '20%',
itemStyle: {
color: 'transparent',
borderColor: 'transparent'
},
emphasis: { itemStyle: { color: 'transparent', borderColor: 'transparent' } },
label: {
show: false,
position: 'insideBottom',
color: '#fff',
fontSize: 11,
fontWeight: 600,
backgroundColor: 'rgba(0,0,0,0.1)',
padding: [2, 3],
borderRadius: 4,
borderColor: 'rgba(255,255,255,0.3)',
borderWidth: 1,
distance: 0,
formatter: (params) => {
const di = wellData.depthIntervals[params.dataIndex];
const value = di?.jc ?? di?.interval;
if (value === undefined || value === null || value === '') return params.data;
return `进尺 ${value}`;
}
},
data: wellData.depthIntervals.map((item) => {
const value = item?.jc ?? item?.interval;
return typeof value === 'number' ? value : Number(value) || 0;
})
}
];
},
// 在现有直方图配置上叠加 6 条曲线:统一 y 轴为深度,新建一条 value 型 x 轴承载数值
extendWithCurves(option, chartConfig) {
const cd = this.curveData;
if (!cd) {
console.warn("HistogramGraph.extendWithCurves: 无 curveData,直接返回");
return;
}
console.log("HistogramGraph.extendWithCurves 收到 curveData:", cd);
// 收集所有数值,确定新 x 轴范围
const collectValues = (arr) =>
(arr || [])
.map((p) => (p && p.value != null ? Number(p.value) : null))
.filter((v) => v != null && !Number.isNaN(v));
const collectDepths = (arr) =>
(arr || [])
.map((p) => (p && p.depth != null ? Number(p.depth) : null))
.filter((v) => v != null && !Number.isNaN(v));
const allVals = [
...collectValues(cd.drillingTime),
...collectValues(cd.torque),
...collectValues(cd.standpipePressure),
...collectValues(cd.drillingPressure),
...collectValues(cd.rpm),
...collectValues(cd.pumpStroke),
...collectValues(cd.rkllData),
];
const allDepths = [
...collectDepths(cd.drillingTime),
...collectDepths(cd.torque),
...collectDepths(cd.standpipePressure),
...collectDepths(cd.drillingPressure),
...collectDepths(cd.rpm),
...collectDepths(cd.pumpStroke),
...collectDepths(cd.rkllData),
];
console.log(
"HistogramGraph.extendWithCurves 所有数值长度:",
allVals.length,
"示例前若干值:",
allVals.slice(0, 20)
);
if (!allVals.length) {
console.warn("HistogramGraph.extendWithCurves: 所有曲线数值为空,不生成折线");
return;
}
if (allDepths.length) {
const minDepthCurve = Math.min(...allDepths);
const maxDepthCurve = Math.max(...allDepths);
console.log("HistogramGraph.extendWithCurves 曲线深度范围:", {
minDepthCurve,
maxDepthCurve,
yAxis0Before: option.yAxis && option.yAxis[0],
});
// 尝试扩展左侧深度轴范围,确保曲线不会被裁剪在 y 轴之外
if (Array.isArray(option.yAxis) && option.yAxis[0]) {
const y0 = option.yAxis[0];
if (typeof y0.min === "number" && typeof y0.max === "number") {
y0.min = Math.min(y0.min, minDepthCurve);
y0.max = Math.max(y0.max, maxDepthCurve);
}
}
}
const minVal = Math.min(...allVals);
const maxVal = Math.max(...allVals);
const span = maxVal - minVal || 1;
const padding = span * 0.1;
const axisMin = minVal - padding;
const axisMax = maxVal + padding;
console.log("HistogramGraph.extendWithCurves x 轴范围:", {
minVal,
maxVal,
axisMin,
axisMax,
});
// 新增一条 value 型 x 轴,放在底部,专门给多曲线使用
const xAxisIndex = Array.isArray(option.xAxis) ? option.xAxis.length : 0;
if (!Array.isArray(option.xAxis)) option.xAxis = [];
option.xAxis.push({
type: "value",
position: "bottom",
name: "钻时 / 扭矩 / 立压 / 钻压 / 转速 / 泵冲 / 入口流量",
nameLocation: "center",
nameGap: 45,
min: axisMin,
max: axisMax,
axisLine: {
show: true,
lineStyle: {
color: "#9ca3af",
width: 1.5,
},
},
axisLabel: {
fontSize: 11,
color: "#4b5563",
},
splitLine: {
show: false,
},
});
if (!Array.isArray(option.series)) option.series = [];
const makeSeriesData = (arr) =>
(arr || [])
.filter(
(p) =>
p &&
p.depth != null &&
p.value != null &&
!Number.isNaN(Number(p.value))
)
.map((p) => [Number(p.value), p.depth])
.sort((a, b) => a[1] - b[1]); // 按深度排序
const pushCurve = (name, arr, color, yAxisIndex, lineWidth = 1) => {
const data = makeSeriesData(arr);
if (!data.length) return;
option.series.push({
name,
type: "line",
xAxisIndex,
yAxisIndex,
// 提高 zlevel,确保在柱子和地层之上渲染
zlevel: 30,
z: 30,
symbol: "none",
smooth: true,
lineStyle: {
color,
width: lineWidth,
},
data,
});
};
// 统一使用左侧深度轴(index 0),保证纵向坐标语义一致
pushCurve("钻时 (min/m)", cd.drillingTime, "#FF0000", 0, 2);
pushCurve("扭矩 (kN•m)", cd.torque, "#1E90FF", 0);
pushCurve("立压 (MPa)", cd.standpipePressure, "#32CD32", 0);
pushCurve("钻压 (kN)", cd.drillingPressure, "#FFD700", 0);
pushCurve("转速 (rpm)", cd.rpm, "#FF6347", 0);
pushCurve("泵冲", cd.pumpStroke, "#9370DB", 0);
pushCurve("入口流量", cd.rkllData, "#20B2AA", 0);
},
// 颜色调整工具方法
adjustColor(color, factor) {
if (color.startsWith('#')) {
const r = parseInt(color.slice(1, 3), 16);
const g = parseInt(color.slice(3, 5), 16);
const b = parseInt(color.slice(5, 7), 16);
const newR = Math.min(255, Math.round(r * factor));
const newG = Math.min(255, Math.round(g * factor));
const newB = Math.min(255, Math.round(b * factor));
return `rgb(${newR}, ${newG}, ${newB})`;
}
return color;
},
// 渲染空状态
renderEmpty(chartDom) {
const colors = this.colorScheme;
chartDom.innerHTML = `
<div class="empty-state" style="
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: ${colors.text};
font-size: 16px;
background: ${colors.background};
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
">
<div style="
font-size: 48px;
margin-bottom: 16px;
opacity: 0.6;
color: ${colors.primary};
">📊</div>
<div style="
font-size: 18px;
color: ${colors.text};
font-weight: 500;
">暂无数据</div>
<div style="
font-size: 14px;
color: ${colors.text};
opacity: 0.6;
margin-top: 8px;
">请检查数据配置</div>
</div>
`;
},
// 处理错误
handleError(error) {
console.error("图表错误:", error);
},
formatXAxisLabel(value) {
if (value == null) return "";
const str = String(value);
const idx = str.indexOf("-");
return idx === -1 ? str : str.slice(0, idx);
},
// 创建SVG图片
createSvgImage(svgString) {
return new Promise((resolve, reject) => {
const svgBlob = new Blob([svgString], { type: 'image/svg+xml;charset=utf-8' });
const svgUrl = URL.createObjectURL(svgBlob);
const img = new Image();
img.onload = () => {
URL.revokeObjectURL(svgUrl);
resolve(img);
};
img.onerror = (e) => {
URL.revokeObjectURL(svgUrl);
reject(e);
};
img.src = svgUrl;
});
},
// 为右侧图例生成纹理样式
getLegendSwatchStyle(item = {}) {
const baseStyle = {
backgroundColor: '#d1d5db',
backgroundRepeat: 'repeat',
backgroundSize: '36px 36px'
};
if (!item?.svg) return baseStyle;
const dataUrl = this.getSvgDataUrl(item.svg);
if (!dataUrl) return baseStyle;
return {
backgroundImage: `url("${dataUrl}")`,
backgroundRepeat: 'repeat',
backgroundSize: '36px 36px',
backgroundColor: 'transparent'
};
},
getSvgDataUrl(svgString) {
if (!svgString || typeof svgString !== 'string') return '';
try {
const compact = svgString.replace(/\s+/g, ' ').trim();
return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(compact)}`;
} catch (e) {
console.warn('SVG 编码失败:', e);
return '';
}
},
// 层位名称不再图内显示,仅清理旧的文字元素
drawStratumLabels() {
if (!this.myChart) return;
const elements = Array.isArray(this.currentGraphicElements)
? this.currentGraphicElements.filter(el => !el.__stratumLabel)
: [];
this.currentGraphicElements = elements;
this.$nextTick(() => {
if (this.myChart) {
this.myChart.setOption({ graphic: { elements } }, { replaceMerge: ['graphic'] });
}
});
},
// 根据 depthIntervals 的顺序按 jh 分段,绘制虚线与顶部井号
drawJhSeparators(depthIntervals, xAxisLabels, chartConfig) {
if (!this.myChart || !depthIntervals || depthIntervals.length === 0) return;
// 扫描 depthIntervals 顺序,形成段
const segments = [];
let currentJh = null;
let currentMin = Infinity;
let currentMax = -Infinity;
let started = false;
for (let i = 0; i < depthIntervals.length; i++) {
// 直接使用在 depthIntervals 中的顺序索引作为 x 轴位置,
// 以避免相同 x 标签造成的覆盖与错位
const { jh } = depthIntervals[i];
const idx = i;
if (!started) {
currentJh = jh;
currentMin = currentMax = idx;
started = true;
continue;
}
if (jh === currentJh) {
currentMin = Math.min(currentMin, idx);
currentMax = Math.max(currentMax, idx);
} else {
segments.push({
jh: currentJh,
startIdx: Math.min(currentMin, currentMax),
endIdx: Math.max(currentMin, currentMax)
});
// 开始新段
currentJh = jh;
currentMin = currentMax = idx;
}
}
if (started) {
segments.push({
jh: currentJh,
startIdx: Math.min(currentMin, currentMax),
endIdx: Math.max(currentMin, currentMax)
});
}
const graphics = [];
// 确保图表已完全渲染,检查容器尺寸
const chartDom = this.$refs.chartRef;
if (!chartDom || chartDom.offsetWidth === 0 || chartDom.offsetHeight === 0) {
console.warn('图表容器尺寸无效,跳过绘制井号分隔符');
return;
}
// 垂直范围(像素)——考虑 y 轴 inverse,取像素最小为顶部、最大为底部
const yMinPx = this.myChart.convertToPixel({ yAxisIndex: 0 }, chartConfig.yAxis.min);
const yMaxPx = this.myChart.convertToPixel({ yAxisIndex: 0 }, chartConfig.yAxis.max);
if (yMinPx === null || yMaxPx === null || isNaN(yMinPx) || isNaN(yMaxPx)) {
console.warn('convertToPixel 返回无效值,跳过绘制井号分隔符');
return;
}
const yTopPx = Math.min(yMinPx, yMaxPx);
const yBottomPx = Math.max(yMinPx, yMaxPx);
// 边界虚线(段与段之间),改为更浅的灰色虚线
for (let i = 0; i < segments.length - 1; i++) {
const leftEnd = segments[i].endIdx;
const rightStart = segments[i + 1].startIdx;
const pxLeft = this.myChart.convertToPixel({ xAxisIndex: 0 }, leftEnd);
const pxRight = this.myChart.convertToPixel({ xAxisIndex: 0 }, rightStart);
if (pxLeft === null || pxRight === null) {
console.warn('convertToPixel 返回 null 值,跳过绘制边界虚线');
continue;
}
const midX = (pxLeft + pxRight) / 2;
graphics.push({
type: 'line',
shape: { x1: midX, y1: yTopPx, x2: midX, y2: yBottomPx },
style: { stroke: '#bbb', lineDash: [8, 6], lineWidth: 1.5 },
silent: true,
z: 100,
zlevel: 10
});
}
// 每段顶部显示 jh(居中),带背景标签并做简单防重叠处理
let lastLabelRight = -Infinity;
let rowShift = 0; // 逐行上移避免覆盖
segments.forEach((seg, idx) => {
if (!seg.jh) return;
const pxStart = this.myChart.convertToPixel({ xAxisIndex: 0 }, seg.startIdx);
const pxEnd = this.myChart.convertToPixel({ xAxisIndex: 0 }, seg.endIdx);
if (pxStart === null || pxEnd === null) {
console.warn('convertToPixel 返回 null 值,跳过绘制井号标签');
return;
}
const midX = (pxStart + pxEnd) / 2;
let topY = yTopPx - 80; // 再上移,提升显示位置
// 估算标签宽度用于避免与上一个重叠
const fontSize = 12;
const paddingH = 6;
const estimatedWidth = seg.jh.length * fontSize * 0.6 + paddingH * 2 + 10;
const labelLeft = midX - estimatedWidth / 2;
const labelRight = midX + estimatedWidth / 2;
if (labelLeft < lastLabelRight) {
rowShift += 18; // 叠一行
} else {
rowShift = 0;
}
lastLabelRight = Math.max(lastLabelRight, labelRight);
topY -= rowShift;
const labelWidth = Math.max(estimatedWidth, 80);
const labelHeight = 22;
const pointerSize = 6;
graphics.push({
type: 'group',
x: midX - labelWidth / 2,
y: topY - labelHeight,
z: 101,
zlevel: 10,
silent: true,
children: [
{
type: 'rect',
shape: { x: 0, y: 0, width: labelWidth, height: labelHeight, r: 8 },
style: {
fill: '#2563eb',
stroke: '#1e40af',
lineWidth: 1.5,
shadowBlur: 6,
shadowColor: 'rgba(37,99,235,0.35)'
}
},
{
type: 'polygon',
shape: {
points: [
[labelWidth / 2 - pointerSize, labelHeight],
[labelWidth / 2 + pointerSize, labelHeight],
[labelWidth / 2, labelHeight + pointerSize]
]
},
style: { fill: '#2563eb', stroke: '#1e40af' }
},
{
type: 'text',
style: {
x: labelWidth / 2,
y: labelHeight / 2,
text: seg.jh,
fill: '#fff',
fontSize: fontSize + 1,
fontWeight: 700,
align: 'center',
verticalAlign: 'middle'
},
zlevel: 40
}
]
});
});
// 叠加到现有 graphic 上(保留原有 graphic)
const prevElements = Array.isArray(this.currentGraphicElements) ? this.currentGraphicElements : [];
const merged = prevElements.concat(graphics);
this.currentGraphicElements = merged;
this.$nextTick(() => {
if (this.myChart) {
this.myChart.setOption({ graphic: { elements: merged } }, { replaceMerge: ['graphic'] });
}
});
},
// 为每个 x 类目在左右各画一条黑色竖线(仅顶部短竖线)
drawCategoryEdgeLines(xAxisLabels, chartConfig) {
if (!this.myChart || !Array.isArray(xAxisLabels) || xAxisLabels.length === 0) return;
// 确保图表已完全渲染,检查容器尺寸
const chartDom = this.$refs.chartRef;
if (!chartDom || chartDom.offsetWidth === 0 || chartDom.offsetHeight === 0) {
console.warn('图表容器尺寸无效,跳过绘制类目边界线');
return;
}
const yMinPx = this.myChart.convertToPixel({ yAxisIndex: 0 }, chartConfig.yAxis.min);
const yMaxPx = this.myChart.convertToPixel({ yAxisIndex: 0 }, chartConfig.yAxis.max);
if (yMinPx === null || yMaxPx === null || isNaN(yMinPx) || isNaN(yMaxPx)) {
console.warn('convertToPixel 返回无效值,跳过绘制类目边界线');
return;
}
const yTopPx = Math.min(yMinPx, yMaxPx);
const yBottomPx = Math.max(yMinPx, yMaxPx);
// 只在上方画短竖线:长度占绘图区高度的 6%,并限制在 15-60 像素
const plotHeight = Math.abs(yBottomPx - yTopPx);
const stemLen = Math.max(15, Math.min(60, plotHeight * 0.06));
// 仅保留上方的线:整段都在 top 之上
const extraHead = Math.max(6, Math.min(30, plotHeight * 0.03));
const yStemStart = yTopPx - (extraHead + stemLen);
const yStemEnd = yTopPx - 1;
const graphics = [];
const n = xAxisLabels.length;
// 计算每个类目的像素中心
const centers = [];
for (let i = 0; i < n; i++) {
const cx = this.myChart.convertToPixel({ xAxisIndex: 0 }, i);
if (cx != null) centers.push(cx);
}
if (centers.length > 0) {
// 第一个点左侧边界:用前两个中心的半步估计
let firstEdge;
if (centers.length >= 2) {
firstEdge = centers[0] - (centers[1] - centers[0]) / 2;
} else {
// 只有一个点时,给一个合理的固定偏移
firstEdge = centers[0] - 40;
}
graphics.push({
type: 'line',
shape: { x1: firstEdge, y1: yStemStart, x2: firstEdge, y2: yStemEnd },
style: { stroke: '#bbb', lineWidth: 1.5 },
silent: true,
z: 120,
zlevel: 10
});
// 相邻两点之间的中点
for (let i = 0; i < centers.length - 1; i++) {
const mid = (centers[i] + centers[i + 1]) / 2;
graphics.push({
type: 'line',
shape: { x1: mid, y1: yStemStart, x2: mid, y2: yStemEnd },
style: { stroke: '#bbb', lineWidth: 1.5 },
silent: true,
z: 120,
zlevel: 10
});
}
// 右侧最后一个边界线:对称估算半步并绘制(补齐“最后那条线”)
let lastEdge;
if (centers.length >= 2) {
lastEdge = centers[centers.length - 1] + (centers[centers.length - 1] - centers[centers.length - 2]) / 2;
} else {
lastEdge = centers[0] + 40;
}
graphics.push({
type: 'line',
shape: { x1: lastEdge, y1: yStemStart, x2: lastEdge, y2: yStemEnd },
style: { stroke: '#bbb', lineWidth: 1.5 },
silent: true,
z: 120,
zlevel: 10
});
}
const prev = Array.isArray(this.currentGraphicElements) ? this.currentGraphicElements : [];
const merged = prev.concat(graphics);
this.currentGraphicElements = merged;
this.$nextTick(() => {
if (this.myChart) {
this.myChart.setOption({ graphic: { elements: merged } }, { replaceMerge: ['graphic'] });
}
});
},
// 图内根据 x 和 y2 渲染 SVG 纹理(每段使用该 x 在 stackedAreas 中最浅层 y2 对应的 svg),从顶部填充到 y2 深度
async drawTopBandSvg(xAxisLabels, chartConfig, stackedAreas) {
if (!this.myChart || !Array.isArray(xAxisLabels) || !stackedAreas) return;
// 确保图表已完全渲染,检查容器尺寸
const chartDom = this.$refs.chartRef;
if (!chartDom || chartDom.offsetWidth === 0 || chartDom.offsetHeight === 0) {
console.warn('图表容器尺寸无效,跳过绘制顶部SVG纹理');
return;
}
// 计算每个类目的像素中心
const centers = [];
for (let i = 0; i < xAxisLabels.length; i++) {
const cx = this.myChart.convertToPixel({ xAxisIndex: 0 }, i);
if (cx != null) centers.push(cx);
}
if (centers.length === 0) return;
// 计算段边界(第一个左边界 + 各中点)
let firstEdge;
if (centers.length >= 2) {
firstEdge = centers[0] - (centers[1] - centers[0]) / 2;
} else {
firstEdge = centers[0] - 40;
}
const boundaries = [firstEdge];
for (let i = 0; i < centers.length - 1; i++) {
boundaries.push((centers[i] + centers[i + 1]) / 2);
}
// 图内纵向像素范围:从图内顶部到 y2 所在像素
const yMinPx = this.myChart.convertToPixel({ yAxisIndex: 0 }, chartConfig.yAxis.min);
const yMaxPx = this.myChart.convertToPixel({ yAxisIndex: 0 }, chartConfig.yAxis.max);
if (yMinPx == null || yMaxPx == null) return;
const yTopPx = Math.min(yMinPx, yMaxPx);
const plotHeight = Math.abs(yMaxPx - yMinPx);
// 摊平 stackedAreas 以便按 x 查找图层(保留全部层,用于全部渲染)
const allLayers = [];
if (Array.isArray(stackedAreas)) {
for (const l of stackedAreas) if (l) allLayers.push(l);
} else {
for (const layers of Object.values(stackedAreas)) {
if (Array.isArray(layers)) allLayers.push(...layers.filter(Boolean));
}
}
const graphics = [];
// 仅在每口井的最后一列标注层名称
const depthIntervals = this.mockData?.wellData?.depthIntervals || [];
const lastIndexByJh = new Map();
for (let idx = 0; idx < depthIntervals.length; idx++) {
const jhVal = depthIntervals[idx]?.jh;
if (jhVal != null) lastIndexByJh.set(jhVal, idx);
}
for (let i = 0; i < xAxisLabels.length; i++) {
const xLabel = String(xAxisLabels[i]);
const left = boundaries[i];
const right = (i < boundaries.length - 1) ? boundaries[i + 1] : left + (centers[1] ? (centers[1] - centers[0]) : 80);
const width = Math.max(10, right - left);
// 找到包含该 x 的所有层,提取该 x 对应点的 y2;若有重叠(相同 y2)则只保留一次
const y2ToLayer = new Map();
for (const layer of allLayers) {
const pts = Array.isArray(layer?.points) ? layer.points : [];
const p = pts.find(p0 => String(p0?.x) === xLabel);
if (!p) continue;
const y2val = Number(p?.y2 ?? layer?.sjdjsd);
if (Number.isNaN(y2val)) continue;
if (!y2ToLayer.has(y2val)) y2ToLayer.set(y2val, layer);
}
const sortedY2 = Array.from(y2ToLayer.keys()).sort((a, b) => a - b);
let prevBottom = null; // 上一个片段的底(y2),为空则从图内顶部开始
for (const y2val of sortedY2) {
const layer = y2ToLayer.get(y2val);
const yTopDepth = prevBottom == null ? chartConfig?.yAxis?.min : prevBottom;
const yBottomDepth = y2val;
const yPixTop = this.myChart.convertToPixel({ yAxisIndex: 0 }, yTopDepth);
const yPixBottom = this.myChart.convertToPixel({ yAxisIndex: 0 }, yBottomDepth);
if (yPixTop == null || yPixBottom == null) {
prevBottom = y2val;
continue;
}
const rectTop = Math.min(yPixTop, yPixBottom);
const rectBottom = Math.max(yPixTop, yPixBottom);
const y1pix = Math.max(yTopPx, rectTop);
const y2pix = Math.min(Math.max(yMinPx, yMaxPx), rectBottom);
const height = Math.max(0, y2pix - y1pix);
if (height > 0) {
let fill = '#d0d3d8';
if (layer?.svg) {
try {
const img = await this.createSvgImage(layer.svg);
fill = { image: img, repeat: 'repeat' };
} catch (e) { /* ignore */
}
}
graphics.push({
type: 'rect',
shape: { x: left, y: y1pix, width, height },
style: { fill, stroke: 'rgba(0,0,0,0.12)', lineWidth: 1 },
silent: true,
z: -10,
zlevel: 1,
__band: true
});
}
prevBottom = y2val;
}
}
// 合并到当前 graphic 上:先移除旧 band,再追加新的,避免重复渲染
const prev = Array.isArray(this.currentGraphicElements) ? this.currentGraphicElements : [];
const kept = prev.filter(el => !el.__band);
const merged = kept.concat(graphics);
this.currentGraphicElements = merged;
this.$nextTick(() => {
if (this.myChart) {
this.myChart.setOption({ graphic: { elements: merged } }, { replaceMerge: ['graphic'] });
}
});
},
// 导出图表为图片
exportChart() {
if (!this.myChart) {
this.$message.warning('图表尚未加载完成,请稍候再试');
return;
}
try {
// 获取图表图片数据
const dataURL = this.myChart.getDataURL({
type: 'png',
pixelRatio: 2, // 提高图片清晰度
backgroundColor: '#fff'
});
// 创建下载链接
const link = document.createElement('a');
const fileName = this.jh ? `直方图_${this.jh}_${new Date().getTime()}.png` : `直方图_${new Date().getTime()}.png`;
link.href = dataURL;
link.download = fileName;
link.style.display = 'none';
// 触发下载
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
this.$message.success('图表导出成功');
} catch (error) {
console.error('导出图表失败:', error);
this.$message.error('导出图表失败,请稍候再试');
}
}
},
};
</script>
<style lang="scss" scoped>
/* 容器样式优化 */
.chart-container {
width: 100%;
min-height: calc(100vh - 140px);
padding: 0 10px;
margin: 0;
box-sizing: border-box;
display: flex;
flex-direction: column;
position: relative;
overflow: auto;
}
.chart-layout {
display: flex;
flex: 1;
width: 100%;
gap: 5px;
align-items: stretch;
justify-content: flex-start;
overflow: hidden;
}
.strata-legend {
width: 240px;
min-width: 240px;
max-height: 100%;
padding: 16px;
border-radius: 16px;
background: #fff;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08);
border: 1px solid rgba(15, 23, 42, 0.06);
display: flex;
flex-direction: column;
gap: 12px;
}
.legend-header {
font-size: 16px;
font-weight: 600;
color: #1f2937;
border-bottom: 1px solid rgba(15, 23, 42, 0.08);
padding-bottom: 8px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.legend-list {
overflow-y: auto;
flex: 1;
display: flex;
flex-direction: column;
gap: 1px;
padding-right: 4px;
}
.legend-item {
display: flex;
align-items: center;
gap: 5px;
padding: 0px 4px;
border-radius: 8px;
transition: background 0.2s ease;
}
.legend-item:hover {
background-color: rgba(59, 130, 246, 0.08);
}
.legend-icon {
width: 40px;
height: 24px;
border-radius: 6px;
border: 1px solid rgba(15, 23, 42, 0.15);
background-color: #d1d5db;
background-size: cover !important;
}
.legend-label {
flex: 1;
font-size: 13px;
font-weight: 500;
color: #1f2937;
}
/* 井号显示样式 */
.well-number-display {
position: absolute;
top: 16px;
left: 16px;
z-index: 5;
background: transparent;
padding: 8px 12px;
display: flex;
align-items: center;
gap: 6px;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
transition: all 0.3s ease;
opacity: 0.9;
}
/* 导出按钮样式 */
::v-deep .export-btn {
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%) !important;
color: #fff !important;
border: none !important;
border-radius: 6px !important;
padding: 6px 14px !important;
font-size: 13px !important;
font-weight: 500 !important;
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3) !important;
transition: all 0.3s ease !important;
}
::v-deep .export-btn:hover:not(:disabled) {
background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%) !important;
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.4) !important;
transform: translateY(-1px) !important;
}
::v-deep .export-btn:active:not(:disabled) {
transform: translateY(0) !important;
box-shadow: 0 2px 6px rgba(59, 130, 246, 0.3) !important;
}
::v-deep .export-btn:disabled {
background: #9ca3af !important;
cursor: not-allowed !important;
opacity: 0.6 !important;
box-shadow: none !important;
}
::v-deep .export-btn .el-icon-download {
color: #fff !important;
}
.well-number-display:hover {
opacity: 1;
}
.well-label {
color: #6b7280;
font-size: 13px;
font-weight: 500;
white-space: nowrap;
}
.well-number {
color: #3b82f6;
font-size: 14px;
font-weight: 600;
}
/* 图表容器样式 */
.chart {
flex: 1;
width: 100%;
max-width: 100%;
height: 100%;
min-height: 400px;
display: block;
border-radius: 16px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
border: 1px solid rgba(255, 255, 255, 0.8);
backdrop-filter: blur(10px);
transition: all 0.3s ease;
}
.chart:hover {
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.15);
/* transform: translateY(-2px); */
}
/* 加载状态样式 */
.loading-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.95);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 10;
border-radius: 16px;
backdrop-filter: blur(10px);
}
.loading-spinner {
width: 48px;
height: 48px;
border: 4px solid #f3f4f6;
border-top: 4px solid #3b82f6;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 16px;
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.2);
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
/* 响应式优化 */
@media (max-width: 768px) {
.chart-container {
padding: 0 10px;
}
.chart {
min-height: 300px;
border-radius: 12px;
}
.export-btn {
padding: 4px 8px;
font-size: 12px;
}
}
/* 深色模式支持 */
@media (prefers-color-scheme: dark) {
.chart {
/* background: linear-gradient(135deg, #1f2937 0%, #111827 100%); */
/* box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4); */
border: 1px solid rgba(255, 255, 255, 0.1);
}
/*
.chart:hover {
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.5);
} */
.loading-overlay {
background: rgba(17, 24, 39, 0.95);
}
}
/* 动画效果 */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.chart-container {
animation: fadeIn 0.6s ease-out;
}
</style>
<template>
<div class="chart-container">
<div class="chart-layout">
<div class="chart-left">
<div id="mainzftdj" class="chart" ref="chartRef"></div>
<div
v-if="wellSegments && wellSegments.length"
class="well-mini-row"
>
<div
v-for="seg in wellSegments"
:key="`${seg.jh}_${seg.startIdx}_${seg.endIdx}`"
class="well-mini-item"
:style="{ flex: `${seg.count} 0 0` }"
>
<div class="well-mini-title">{{ seg.jh || "-" }}</div>
<WellDrillingTimeMiniChart
:jh="seg.jh"
:rows="getLjRowsByJh(seg.jh)"
:height="miniChartsHeight"
/>
</div>
</div>
</div>
<aside v-if="legendItems && legendItems.length" class="strata-legend">
<div class="legend-header">
<span>层位图例</span>
<el-button class="export-btn" @click="exportChart" :disabled="loading || !myChart" title="导出图表为图片"
icon="el-icon-download" size="small">
导出
</el-button>
</div>
<div class="legend-list">
<div v-for="(item, index) in legendItems" :key="item.name || index" class="legend-item">
<div class="legend-icon" :style="getLegendSwatchStyle(item)"></div>
<span class="legend-label">{{ item.name || '-' }}</span>
</div>
</div>
</aside>
</div>
<div v-if="loading" class="loading-overlay">
<div class="loading-spinner"></div>
<span>加载中...</span>
</div>
</div>
</template>
<script>
import * as echarts from "echarts";
import { getdjZft } from "@/api/optimization/initialization";
import { text } from "d3";
import WellDrillingTimeMiniChart from "./WellDrillingTimeMiniChart.vue";
export default {
name: "HistogramGraph",
components: { WellDrillingTimeMiniChart },
props: {
jh: {
type: String,
default: "",
},
jhs: {
type: String,
default: ''
},
// 美化配置选项
theme: {
type: String,
default: "modern", // modern, elegant, vibrant
validator: value => ["modern", "elegant", "vibrant"].includes(value)
}
},
data() {
return {
mockData: {},
legendItems: [],
myChart: null,
initRetryCount: 0,
maxRetryCount: 5,
resizeObserver: null,
loading: false,
debounceTimer: null,
lastStackedAreas: null,
lastChartConfig: null,
lastXAxisLabels: null,
lastDepthIntervals: null,
currentGraphicElements: [],
wellSegments: [],
miniChartsHeight: 180,
};
},
computed: {
// 根据主题获取颜色配置
colorScheme() {
const schemes = {
modern: {
primary: "#3B82F6",
secondary: "#10B981",
accent: "#F59E0B",
background: "#F8FAFC",
text: "#1F2937",
border: "#E5E7EB",
gradient: {
start: "#3B82F6",
end: "#1D4ED8"
}
},
elegant: {
primary: "#6366F1",
secondary: "#8B5CF6",
accent: "#EC4899",
background: "#FAFAFA",
text: "#374151",
border: "#D1D5DB",
gradient: {
start: "#6366F1",
end: "#4F46E5"
}
},
vibrant: {
primary: "#EF4444",
secondary: "#06B6D4",
accent: "#F97316",
background: "#FEFEFE",
text: "#111827",
border: "#F3F4F6",
gradient: {
start: "#EF4444",
end: "#DC2626"
}
}
};
return schemes[this.theme];
},
legendPanelWidth() {
return this.legendItems && this.legendItems.length ? 260 : 0;
}
},
watch: {
jh: {
handler(newVal) {
// 不自动刷新,等待父级触发 loadData
},
immediate: false
},
legendItems: {
handler() {
this.$nextTick(() => {
const chartDom = this.$refs.chartRef;
if (chartDom) {
this.setChartDimensions(chartDom);
this.performResize();
}
});
},
deep: true
}
},
mounted() {
// 初始化空图表,不拉数据;等待父组件触发 loadData
this.initChart();
this.setupEventListeners();
},
beforeDestroy() {
this.cleanup();
},
methods: {
// 初始化图表
async initChart() {
try {
this.loading = true;
const chartDom = this.$refs.chartRef;
if (!chartDom) {
throw new Error("未找到图表容器 DOM");
}
this.setChartDimensions(chartDom);
// 重试机制优化
if (this.shouldRetry()) {
this.scheduleRetry();
return;
}
this.resetRetryCount();
this.disposeChart();
this.createChart(chartDom);
const mockData = await this.getList();
if (!this.hasValidData(mockData)) {
this.wellSegments = [];
this.renderEmpty(chartDom);
return;
}
this.wellSegments = this.computeWellSegments(mockData?.wellData?.depthIntervals);
await this.renderRealData(mockData);
} catch (error) {
console.error("图表初始化失败:", error);
this.handleError(error);
} finally {
this.loading = false;
}
},
// 设置图表尺寸
setChartDimensions(chartDom) {
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
const containerWidth = this.$el?.clientWidth || viewportWidth;
const rect = this.$el?.getBoundingClientRect();
const topOffset = rect ? rect.top : 0;
const legendWidth = this.legendPanelWidth || 0;
const containerPadding = 20; // chart-container 水平 padding 之和
const panelGap = legendWidth ? 5 : 0; // 与右侧图例的间距
const safetyMargin = 12; // 额外预留
const widthPadding = containerPadding + panelGap + legendWidth + safetyMargin;
const availableWidth = Math.max(360, containerWidth - widthPadding);
const verticalPadding = 40;
const heightByViewport = viewportHeight - topOffset - verticalPadding;
const fallbackHeight = this.$el?.clientHeight || viewportHeight;
const miniRowExtra =
this.wellSegments && this.wellSegments.length
? (Number(this.miniChartsHeight) || 180) + 44 // 标题/间距预留
: 0;
const availableHeight = Math.max(360, heightByViewport - miniRowExtra, fallbackHeight - 20 - miniRowExtra);
chartDom.style.width = `${availableWidth}px`;
chartDom.style.height = `${availableHeight}px`;
chartDom.offsetHeight; // 强制重排
},
// 检查是否需要重试
shouldRetry() {
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
const availableHeight = viewportHeight - 120;
const availableWidth = viewportWidth - 80;
return (availableWidth <= 0 || availableHeight <= 0) && this.initRetryCount < this.maxRetryCount;
},
// 安排重试
scheduleRetry() {
this.initRetryCount++;
setTimeout(() => this.initChart(), 500);
},
// 重置重试计数
resetRetryCount() {
this.initRetryCount = 0;
},
// 销毁图表
disposeChart() {
if (this.myChart) {
this.myChart.dispose();
}
},
// 创建图表实例
createChart(chartDom) {
this.myChart = echarts.init(chartDom, null, {
renderer: 'canvas',
useDirtyRect: true
});
},
// 检查数据有效性
hasValidData(mockData) {
return mockData &&
mockData.wellData &&
mockData.wellData.depthLine &&
mockData.wellData.depthLine.length > 0;
},
// 设置事件监听器
setupEventListeners() {
window.addEventListener("resize", this.handleResize);
this.observeParentResize();
},
// 清理资源
cleanup() {
window.removeEventListener("resize", this.handleResize);
if (this.myChart) {
this.myChart.dispose();
}
if (this.resizeObserver) {
this.resizeObserver.disconnect();
}
if (this.debounceTimer) {
clearTimeout(this.debounceTimer);
}
},
// 观察父容器大小变化
observeParentResize() {
const parentDom = this.$refs.chartRef?.parentElement;
if (parentDom && window.ResizeObserver) {
this.resizeObserver = new ResizeObserver(() => {
this.debouncedResize();
});
this.resizeObserver.observe(parentDom);
}
},
// 获取数据
async getList() {
try {
const res = await getdjZft({
jhs: `${this.jh},${this.jhs}`
});
this.mockData = res?.mockData || {};
const legendList = Array.isArray(res?.tlList)
? res.tlList
: Array.isArray(res?.mockData?.tlList)
? res.mockData.tlList
: [];
this.legendItems = legendList;
return this.mockData;
} catch (error) {
console.error("获取数据失败:", error);
throw error;
}
},
getLjRowsByJh(jh) {
if (!jh) return [];
const map = this.mockData?.ljJhMap;
if (map && typeof map === "object" && Array.isArray(map[jh])) return map[jh];
return [];
},
computeWellSegments(depthIntervals) {
if (!Array.isArray(depthIntervals) || depthIntervals.length === 0) return [];
const segments = [];
let currentJh = null;
let startIdx = 0;
for (let i = 0; i < depthIntervals.length; i++) {
const jh = depthIntervals[i]?.jh ?? depthIntervals[i]?.x;
if (i === 0) {
currentJh = jh;
startIdx = 0;
continue;
}
if (jh !== currentJh) {
const endIdx = i - 1;
const count = Math.max(1, endIdx - startIdx + 1);
segments.push({ jh: currentJh, startIdx, endIdx, count });
currentJh = jh;
startIdx = i;
}
}
const endIdx = depthIntervals.length - 1;
const count = Math.max(1, endIdx - startIdx + 1);
segments.push({ jh: currentJh, startIdx, endIdx, count });
return segments.filter((s) => s.jh != null && String(s.jh).trim() !== "");
},
// 供父组件在切换到本tab时调用
async loadData() {
await this.initChart();
},
// 处理窗口大小变化
handleResize() {
this.debouncedResize();
},
// 防抖处理resize
debouncedResize() {
if (this.debounceTimer) {
clearTimeout(this.debounceTimer);
}
this.debounceTimer = setTimeout(() => {
this.performResize();
}, 200);
},
// 执行resize操作
performResize() {
if (this.myChart) {
const chartDom = this.$refs.chartRef;
if (chartDom) {
this.setChartDimensions(chartDom);
}
this.$nextTick(() => {
if (this.myChart) {
this.myChart.resize();
if (this.lastStackedAreas && this.lastChartConfig && this.lastXAxisLabels) {
this.drawStratumLabels(this.lastStackedAreas, this.lastChartConfig, this.lastXAxisLabels);
}
if (this.lastDepthIntervals && this.lastXAxisLabels && this.lastChartConfig) {
this.drawJhSeparators(this.lastDepthIntervals, this.lastXAxisLabels, this.lastChartConfig);
}
if (this.lastXAxisLabels && this.lastChartConfig) {
this.drawCategoryEdgeLines(this.lastXAxisLabels, this.lastChartConfig);
this.drawTopBandSvg(this.lastXAxisLabels, this.lastChartConfig, this.lastStackedAreas);
}
}
});
}
},
// 刷新图表
async refreshChart() {
if (this.myChart) {
await this.initChart();
}
},
// 渲染真实数据
async renderRealData(mockData) {
try {
const { wellData } = mockData;
console.log(wellData, 'wellData');
const xAxisLabels = wellData.depthLine.map((point) => point.x);
try {
window.__hist_xLabels = xAxisLabels;
} catch (e) {
}
const option = {
...this.getDefaultChartOption(),
xAxis: this.createXAxis(xAxisLabels),
yAxis: this.createYAxis(mockData.chartConfig),
series: this.createSeries(wellData)
};
const chartDom = this.$refs.chartRef;
if (chartDom) {
this.setChartDimensions(chartDom);
}
this.myChart.setOption(option, true);
// 确保图表完全渲染后再resize和绘制
this.$nextTick(() => {
// 先resize确保图表尺寸正确
this.myChart.resize();
// 使用 requestAnimationFrame 确保浏览器完成渲染后再绘制
requestAnimationFrame(() => {
requestAnimationFrame(() => {
// 再次确认尺寸,确保图表已完全渲染
const chartDom = this.$refs.chartRef;
if (chartDom && chartDom.offsetWidth > 0 && chartDom.offsetHeight > 0) {
// 再次resize确保尺寸准确
this.myChart.resize();
// 保存数据用于后续resize时重新绘制
this.lastStackedAreas = wellData.stackedAreas;
this.lastChartConfig = mockData.chartConfig;
this.lastXAxisLabels = xAxisLabels;
this.lastDepthIntervals = wellData.depthIntervals || [];
// 绘制所有图形元素
this.drawStratumLabels(this.lastStackedAreas, this.lastChartConfig, this.lastXAxisLabels);
this.drawJhSeparators(this.lastDepthIntervals, this.lastXAxisLabels, this.lastChartConfig);
this.drawCategoryEdgeLines(this.lastXAxisLabels, this.lastChartConfig);
this.drawTopBandSvg(this.lastXAxisLabels, this.lastChartConfig, this.lastStackedAreas);
} else {
// 如果尺寸还未确定,延迟重试
setTimeout(() => {
if (this.myChart) {
this.myChart.resize();
this.lastStackedAreas = wellData.stackedAreas;
this.lastChartConfig = mockData.chartConfig;
this.lastXAxisLabels = xAxisLabels;
this.lastDepthIntervals = wellData.depthIntervals || [];
this.drawStratumLabels(this.lastStackedAreas, this.lastChartConfig, this.lastXAxisLabels);
this.drawJhSeparators(this.lastDepthIntervals, this.lastXAxisLabels, this.lastChartConfig);
this.drawCategoryEdgeLines(this.lastXAxisLabels, this.lastChartConfig);
this.drawTopBandSvg(this.lastXAxisLabels, this.lastChartConfig, this.lastStackedAreas);
}
}, 150);
}
});
});
});
} catch (error) {
console.error("渲染数据失败:", error);
this.handleError(error);
}
},
// 获取默认图表配置
getDefaultChartOption() {
const colors = this.colorScheme;
return {
title: { text: "", subtext: "" },
aria: { enabled: false },
backgroundColor: colors.background,
tooltip: {
trigger: "axis",
axisPointer: {
type: "cross",
label: { backgroundColor: colors.primary, show: false },
crossStyle: { color: colors.border }
},
backgroundColor: 'rgba(255,255,255,0.95)',
borderColor: colors.border,
borderWidth: 1,
textStyle: { color: colors.text },
extraCssText: 'box-shadow: 0 4px 12px rgba(0,0,0,0.15); border-radius: 8px;',
formatter: (params) => {
if (!params || params.length === 0) return '';
const dataIndex = params[0].dataIndex;
const depthInterval = this.mockData?.wellData?.depthIntervals?.[dataIndex];
if (!depthInterval) {
// 如果没有depthInterval数据,使用默认格式
let result = `<div style="font-weight: 600; margin-bottom: 8px; color: ${colors.primary};">${params[0].axisValue}</div>`;
params.forEach(param => {
const color = param.color;
result += `<div style="margin: 4px 0;">
<span style="display: inline-block; width: 10px; height: 10px; background-color: ${color}; border-radius: 50%; margin-right: 8px;"></span>
<span style="font-weight: 500;">${param.seriesName}:</span>
<span style="margin-left: 8px; font-weight: 600;">${param.value}</span>
</div>`;
});
return result;
}
// 显示井号信息
let result = `<div style="font-weight: 600; margin-bottom: 8px; color: ${colors.primary};">${depthInterval.jh || depthInterval.x}</div>`;
// 显示详细信息
const modelLabel = this.formatXAxisLabel(depthInterval.x || '-');
result += `<div style="margin: 4px 0; padding: 4px 0; border-top: 1px solid ${colors.border};">
<div style="margin: 2px 0;"><span style="font-weight: 500;">型号:</span> <span style="margin-left: 8px; font-weight: 600;">${modelLabel}</span></div>
<div style="margin: 2px 0;"><span style="font-weight: 500;">尺寸:</span> <span style="margin-left: 8px; font-weight: 600;">${depthInterval.ztcc || '-'}</span></div>
<div style="margin: 2px 0;"><span style="font-weight: 500;">进尺mm:</span> <span style="margin-left: 8px; font-weight: 600;">${depthInterval.jc || '-'}</span></div>
<div style="margin: 2px 0;"><span style="font-weight: 500;">深度区间:</span> <span style="margin-left: 8px; font-weight: 600;">${depthInterval.interval || '-'}</span></div>
</div>`;
// 只显示非地层信息的系列数据(过滤掉地层相关的系列)
// params.forEach(param => {
// // 过滤掉地层相关的系列名称
// if (param.seriesName && !param.seriesName.includes('地层') && param.seriesName !== '占位') {
// const color = param.color;
// result += `<div style="margin: 4px 0;">
// <span style="display: inline-block; width: 10px; height: 10px; background-color: ${color}; border-radius: 50%; margin-right: 8px;"></span>
// <span style="font-weight: 500;">${param.seriesName}:</span>
// <span style="margin-left: 8px; font-weight: 600;">${param.value}</span>
// </div>`;
// }
// });
return result;
}
},
grid: {
top: 20,
left: "2%",
right: "3%",
bottom: "10%",
containLabel: true,
show: false,
},
animation: true,
animationDuration: 1500,
animationEasing: 'cubicOut',
animationDelay: function (idx) {
return idx * 100;
}
};
},
// 处理区域系列
async processAreaSeries(wellData, xAxisLabels) {
const stackedAreas = wellData?.stackedAreas || [];
// 兼容旧格式:数组(所有井已按全局顺序平铺)
if (Array.isArray(stackedAreas)) {
return await Promise.all(
stackedAreas
.filter(area => area.svg !== null)
.map(async (area) => {
let areaStyle = { opacity: 0.6 };
if (area.svg) {
try {
const svgImage = await this.createSvgImage(area.svg);
areaStyle.color = { image: svgImage, repeat: "repeat" };
} catch (error) {
console.error('SVG转换失败:', error);
}
}
return {
name: area.name,
type: "line",
xAxisIndex: 1,
z: -1,
stack: "总量",
symbol: "none",
showSymbol: false,
areaStyle: areaStyle,
lineStyle: { width: 0 },
data: area.points.map((point) => (point && point.y2 != null) ? point.y2 : point.y),
};
})
);
}
// 新格式:对象,key 为井号,每口井内是该井的地层数组
const totalLen = Array.isArray(xAxisLabels) ? xAxisLabels.length : 0;
console.log(totalLen, 'totalLen');
// 全局 x 标签 -> 索引数组 映射
const xToIndices = new Map();
console.log(xToIndices, 'xToIndices');
(xAxisLabels || []).forEach((x, idx) => {
const key = String(x);
if (!xToIndices.has(key)) xToIndices.set(key, []);
xToIndices.get(key).push(idx);
});
const buildNullArray = (len) => Array.from({ length: len }, () => null);
console.log(buildNullArray, 'buildNullArray');
const results = [];
for (const [jh, layers] of Object.entries(stackedAreas)) {
if (!Array.isArray(layers) || layers.length === 0) continue;
// 估算该井的横向覆盖范围:取该井所有层的所有点对应的全局索引的 min/max
const indicesForJh = [];
const missingInXAxis = [];
for (const area of layers) {
for (const p of (area?.points || [])) {
const arr = xToIndices.get(String(p?.x)) || [];
if (arr.length === 0) {
missingInXAxis.push({ x: p?.x, jh, areaName: area?.name });
} else {
indicesForJh.push(...arr);
}
}
}
const startIdx = Math.min(...indicesForJh);
const endIdx = Math.max(...indicesForJh);
for (const area of (layers || [])) {
if (area == null) continue;
let areaStyle = { opacity: 0.6 };
if (area.svg) {
try {
const svgImage = await this.createSvgImage(area.svg);
areaStyle.color = { image: svgImage, repeat: "repeat" };
} catch (error) {
console.error('SVG转换失败:', error);
}
}
const data = buildNullArray(totalLen);
// 把该层每个点的 y2/y 填到该点 x 对应的全局索引中,仅限落在该井的覆盖范围内
const outOfRangePoints = [];
for (const p of (area.points || [])) {
const yVal = (p && p.y2 != null) ? p.y2 : p?.y;
const cands = xToIndices.get(String(p?.x)) || [];
if (cands.length === 0) continue;
let placed = false;
cands.forEach(ix => {
if (ix >= startIdx && ix <= endIdx) {
data[ix] = yVal;
placed = true;
}
});
if (!placed) {
outOfRangePoints.push({ x: p?.x, cands, jh, areaName: area?.name, startIdx, endIdx });
}
}
results.push({
name: area.name,
type: "line",
xAxisIndex: 1,
z: -1,
stack: "总量",
symbol: "none",
showSymbol: false,
areaStyle: areaStyle,
lineStyle: { width: 0 },
data
});
// 仅在有异常时输出调试信息,方便定位 x 匹配问题
if (missingInXAxis.length > 0 || outOfRangePoints.length > 0) {
// 使用 console.group 让日志更清晰
console.groupCollapsed && console.groupCollapsed(`层定位告警: 井号=${jh}`);
if (missingInXAxis.length > 0) {
console.warn('x 不在 xAxisLabels 中:', missingInXAxis);
}
if (outOfRangePoints.length > 0) {
console.warn('x 命中但不在井段范围内:', outOfRangePoints);
console.info('井段范围:', { jh, startIdx, endIdx });
}
console.groupEnd && console.groupEnd();
}
}
}
return results;
},
// 创建X轴配置
createXAxis(xAxisLabels) {
const colors = this.colorScheme;
const formatLabel = (value) => this.formatXAxisLabel(value);
return [
{
type: "category",
boundaryGap: true,
position: "top",
data: xAxisLabels,
axisLabel: {
interval: 0,
rotate: -90,
margin: 30,
align: "center",
fontSize: 12,
color: colors.text,
fontWeight: 500,
formatter: formatLabel
},
axisTick: {
alignWithLabel: true,
length: 6,
lineStyle: { color: colors.border }
},
splitLine: { show: false },
axisLine: {
show: true,
lineStyle: {
color: colors.border,
width: 2
}
}
},
{
type: "category",
boundaryGap: false,
position: "top",
show: false,
data: xAxisLabels
},
];
},
// 创建Y轴配置
createYAxis(chartConfig) {
const colors = this.colorScheme;
return [
{
type: "value",
name: "井深(m)",
nameTextStyle: {
color: colors.text,
fontSize: 13,
fontWeight: 600,
padding: [0, 0, 0, 8]
},
min: chartConfig.yAxis.min,
max: chartConfig.yAxis.max,
interval: chartConfig.yAxis.interval,
inverse: true,
axisLabel: {
formatter: "{value}",
fontSize: 12,
color: colors.text,
fontWeight: 500
},
splitLine: {
show: true,
lineStyle: {
color: colors.border,
type: 'dashed',
opacity: 0.3
}
},
axisLine: {
show: true,
lineStyle: {
color: colors.border,
width: 2
}
},
axisTick: {
show: true,
lineStyle: { color: colors.border }
}
},
{
type: "value",
name: "深度(m)",
nameTextStyle: {
color: colors.text,
fontSize: 13,
fontWeight: 600,
padding: [0, 8, 0, 55]
},
min: chartConfig.yAxis2.min,
max: chartConfig.yAxis2.max,
interval: chartConfig.yAxis2.interval,
position: "right",
axisLabel: {
formatter: "{value}",
fontSize: 12,
color: colors.text,
fontWeight: 500
},
splitLine: { show: false },
axisLine: {
show: true,
lineStyle: {
color: colors.border,
width: 2
}
},
axisTick: {
show: true,
lineStyle: { color: colors.border }
}
},
];
},
// 创建系列配置
createSeries(wellData, areaSeries) {
const colors = this.colorScheme;
return [
{
name: "井深数据",
type: "line",
zlevel: 25,
yAxisIndex: 1,
symbol: "circle",
symbolSize: 10,
itemStyle: {
color: colors.accent,
borderColor: '#fff',
borderWidth: 3,
shadowColor: 'rgba(0,0,0,0.2)',
shadowBlur: 8
},
lineStyle: {
color: colors.accent,
width: 3,
type: 'solid',
shadowColor: 'rgba(0,0,0,0.1)',
shadowBlur: 4
},
label: {
show: true,
position: "top",
formatter: "{c}",
fontSize: 13,
fontWeight: 600,
color: colors.accent,
backgroundColor: "rgba(255,255,255,0.95)",
padding: [6, 8],
borderRadius: 6,
borderColor: colors.accent,
borderWidth: 2,
shadowColor: 'rgba(0,0,0,0.1)',
shadowBlur: 4
},
data: wellData.depthLine.map((point) => point.y),
},
{
name: "占位",
type: "bar",
zlevel: 20,
stack: "total",
silent: true,
barWidth: "20%",
itemStyle: {
borderColor: "transparent",
color: "transparent"
},
emphasis: {
itemStyle: {
borderColor: "transparent",
color: "transparent"
}
},
data: wellData.depthIntervals.map((item) => item.placeholder),
},
{
name: "深度区间",
type: "bar",
zlevel: 20,
stack: "total",
barWidth: "15%",
label: {
show: true,
position: 'insideTop',
color: '#fff',
fontSize: 11,
fontWeight: 600,
backgroundColor: 'rgba(0,0,0,0.1)',
padding: [2, 3],
borderRadius: 4,
borderColor: 'rgba(255,255,255,0.3)',
borderWidth: 1,
formatter: (params) => {
const di = wellData.depthIntervals[params.dataIndex];
const value = di?.jc ?? di?.interval;
if (value === undefined || value === null || value === '') return params.data;
return `${value}`;
}
},
itemStyle: {
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{ offset: 0, color: colors.gradient.start },
{ offset: 1, color: colors.gradient.end }
]
},
borderRadius: [4, 4, 0, 0],
shadowColor: 'rgba(0,0,0,0.2)',
shadowBlur: 8,
shadowOffsetY: 2
},
emphasis: {
itemStyle: {
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{ offset: 0, color: this.adjustColor(colors.gradient.start, 1.2) },
{ offset: 1, color: this.adjustColor(colors.gradient.end, 1.2) }
]
},
shadowBlur: 12,
shadowOffsetY: 4
}
},
data: wellData.depthIntervals.map((item) => {
const value = item?.jc ?? item?.interval;
return typeof value === 'number' ? value : Number(value) || 0;
}),
},
// 底部显示 interval 数值的透明条,用于放置底部标签
{
name: '深度区间-底部标签',
type: 'bar',
zlevel: 20,
// 与主柱重叠以便把标签放在柱底部区域内
barGap: '-100%',
barWidth: '20%',
itemStyle: {
color: 'transparent',
borderColor: 'transparent'
},
emphasis: { itemStyle: { color: 'transparent', borderColor: 'transparent' } },
label: {
show: false,
position: 'insideBottom',
color: '#fff',
fontSize: 11,
fontWeight: 600,
backgroundColor: 'rgba(0,0,0,0.1)',
padding: [2, 3],
borderRadius: 4,
borderColor: 'rgba(255,255,255,0.3)',
borderWidth: 1,
distance: 0,
formatter: (params) => {
const di = wellData.depthIntervals[params.dataIndex];
const value = di?.jc ?? di?.interval;
if (value === undefined || value === null || value === '') return params.data;
return `进尺 ${value}`;
}
},
data: wellData.depthIntervals.map((item) => {
const value = item?.jc ?? item?.interval;
return typeof value === 'number' ? value : Number(value) || 0;
})
}
];
},
// 颜色调整工具方法
adjustColor(color, factor) {
if (color.startsWith('#')) {
const r = parseInt(color.slice(1, 3), 16);
const g = parseInt(color.slice(3, 5), 16);
const b = parseInt(color.slice(5, 7), 16);
const newR = Math.min(255, Math.round(r * factor));
const newG = Math.min(255, Math.round(g * factor));
const newB = Math.min(255, Math.round(b * factor));
return `rgb(${newR}, ${newG}, ${newB})`;
}
return color;
},
// 渲染空状态
renderEmpty(chartDom) {
const colors = this.colorScheme;
chartDom.innerHTML = `
<div class="empty-state" style="
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: ${colors.text};
font-size: 16px;
background: ${colors.background};
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
">
<div style="
font-size: 48px;
margin-bottom: 16px;
opacity: 0.6;
color: ${colors.primary};
">📊</div>
<div style="
font-size: 18px;
color: ${colors.text};
font-weight: 500;
">暂无数据</div>
<div style="
font-size: 14px;
color: ${colors.text};
opacity: 0.6;
margin-top: 8px;
">请检查数据配置</div>
</div>
`;
},
// 处理错误
handleError(error) {
console.error("图表错误:", error);
},
formatXAxisLabel(value) {
if (value == null) return "";
const str = String(value);
const idx = str.indexOf("-");
return idx === -1 ? str : str.slice(0, idx);
},
// 创建SVG图片
createSvgImage(svgString) {
return new Promise((resolve, reject) => {
const svgBlob = new Blob([svgString], { type: 'image/svg+xml;charset=utf-8' });
const svgUrl = URL.createObjectURL(svgBlob);
const img = new Image();
img.onload = () => {
URL.revokeObjectURL(svgUrl);
resolve(img);
};
img.onerror = (e) => {
URL.revokeObjectURL(svgUrl);
reject(e);
};
img.src = svgUrl;
});
},
// 为右侧图例生成纹理样式
getLegendSwatchStyle(item = {}) {
const baseStyle = {
backgroundColor: '#d1d5db',
backgroundRepeat: 'repeat',
backgroundSize: '36px 36px'
};
if (!item?.svg) return baseStyle;
const dataUrl = this.getSvgDataUrl(item.svg);
if (!dataUrl) return baseStyle;
return {
backgroundImage: `url("${dataUrl}")`,
backgroundRepeat: 'repeat',
backgroundSize: '36px 36px',
backgroundColor: 'transparent'
};
},
getSvgDataUrl(svgString) {
if (!svgString || typeof svgString !== 'string') return '';
try {
const compact = svgString.replace(/\s+/g, ' ').trim();
return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(compact)}`;
} catch (e) {
console.warn('SVG 编码失败:', e);
return '';
}
},
// 层位名称不再图内显示,仅清理旧的文字元素
drawStratumLabels() {
if (!this.myChart) return;
const elements = Array.isArray(this.currentGraphicElements)
? this.currentGraphicElements.filter(el => !el.__stratumLabel)
: [];
this.currentGraphicElements = elements;
this.$nextTick(() => {
if (this.myChart) {
this.myChart.setOption({ graphic: { elements } }, { replaceMerge: ['graphic'] });
}
});
},
// 根据 depthIntervals 的顺序按 jh 分段,绘制虚线与顶部井号
drawJhSeparators(depthIntervals, xAxisLabels, chartConfig) {
if (!this.myChart || !depthIntervals || depthIntervals.length === 0) return;
// 扫描 depthIntervals 顺序,形成段
const segments = [];
let currentJh = null;
let currentMin = Infinity;
let currentMax = -Infinity;
let started = false;
for (let i = 0; i < depthIntervals.length; i++) {
// 直接使用在 depthIntervals 中的顺序索引作为 x 轴位置,
// 以避免相同 x 标签造成的覆盖与错位
const { jh } = depthIntervals[i];
const idx = i;
if (!started) {
currentJh = jh;
currentMin = currentMax = idx;
started = true;
continue;
}
if (jh === currentJh) {
currentMin = Math.min(currentMin, idx);
currentMax = Math.max(currentMax, idx);
} else {
segments.push({
jh: currentJh,
startIdx: Math.min(currentMin, currentMax),
endIdx: Math.max(currentMin, currentMax)
});
// 开始新段
currentJh = jh;
currentMin = currentMax = idx;
}
}
if (started) {
segments.push({
jh: currentJh,
startIdx: Math.min(currentMin, currentMax),
endIdx: Math.max(currentMin, currentMax)
});
}
const graphics = [];
// 确保图表已完全渲染,检查容器尺寸
const chartDom = this.$refs.chartRef;
if (!chartDom || chartDom.offsetWidth === 0 || chartDom.offsetHeight === 0) {
console.warn('图表容器尺寸无效,跳过绘制井号分隔符');
return;
}
// 垂直范围(像素)——考虑 y 轴 inverse,取像素最小为顶部、最大为底部
const yMinPx = this.myChart.convertToPixel({ yAxisIndex: 0 }, chartConfig.yAxis.min);
const yMaxPx = this.myChart.convertToPixel({ yAxisIndex: 0 }, chartConfig.yAxis.max);
if (yMinPx === null || yMaxPx === null || isNaN(yMinPx) || isNaN(yMaxPx)) {
console.warn('convertToPixel 返回无效值,跳过绘制井号分隔符');
return;
}
const yTopPx = Math.min(yMinPx, yMaxPx);
const yBottomPx = Math.max(yMinPx, yMaxPx);
// 边界虚线(段与段之间),改为更浅的灰色虚线
for (let i = 0; i < segments.length - 1; i++) {
const leftEnd = segments[i].endIdx;
const rightStart = segments[i + 1].startIdx;
const pxLeft = this.myChart.convertToPixel({ xAxisIndex: 0 }, leftEnd);
const pxRight = this.myChart.convertToPixel({ xAxisIndex: 0 }, rightStart);
if (pxLeft === null || pxRight === null) {
console.warn('convertToPixel 返回 null 值,跳过绘制边界虚线');
continue;
}
const midX = (pxLeft + pxRight) / 2;
graphics.push({
type: 'line',
shape: { x1: midX, y1: yTopPx, x2: midX, y2: yBottomPx },
style: { stroke: '#bbb', lineDash: [8, 6], lineWidth: 1.5 },
silent: true,
z: 100,
zlevel: 10
});
}
// 每段顶部显示 jh(居中),带背景标签并做简单防重叠处理
let lastLabelRight = -Infinity;
let rowShift = 0; // 逐行上移避免覆盖
segments.forEach((seg, idx) => {
if (!seg.jh) return;
const pxStart = this.myChart.convertToPixel({ xAxisIndex: 0 }, seg.startIdx);
const pxEnd = this.myChart.convertToPixel({ xAxisIndex: 0 }, seg.endIdx);
if (pxStart === null || pxEnd === null) {
console.warn('convertToPixel 返回 null 值,跳过绘制井号标签');
return;
}
const midX = (pxStart + pxEnd) / 2;
let topY = yTopPx - 80; // 再上移,提升显示位置
// 估算标签宽度用于避免与上一个重叠
const fontSize = 12;
const paddingH = 6;
const estimatedWidth = seg.jh.length * fontSize * 0.6 + paddingH * 2 + 10;
const labelLeft = midX - estimatedWidth / 2;
const labelRight = midX + estimatedWidth / 2;
if (labelLeft < lastLabelRight) {
rowShift += 18; // 叠一行
} else {
rowShift = 0;
}
lastLabelRight = Math.max(lastLabelRight, labelRight);
topY -= rowShift;
const labelWidth = Math.max(estimatedWidth, 80);
const labelHeight = 22;
const pointerSize = 6;
graphics.push({
type: 'group',
x: midX - labelWidth / 2,
y: topY - labelHeight,
z: 101,
zlevel: 10,
silent: true,
children: [
{
type: 'rect',
shape: { x: 0, y: 0, width: labelWidth, height: labelHeight, r: 8 },
style: {
fill: '#2563eb',
stroke: '#1e40af',
lineWidth: 1.5,
shadowBlur: 6,
shadowColor: 'rgba(37,99,235,0.35)'
}
},
{
type: 'polygon',
shape: {
points: [
[labelWidth / 2 - pointerSize, labelHeight],
[labelWidth / 2 + pointerSize, labelHeight],
[labelWidth / 2, labelHeight + pointerSize]
]
},
style: { fill: '#2563eb', stroke: '#1e40af' }
},
{
type: 'text',
style: {
x: labelWidth / 2,
y: labelHeight / 2,
text: seg.jh,
fill: '#fff',
fontSize: fontSize + 1,
fontWeight: 700,
align: 'center',
verticalAlign: 'middle'
},
zlevel: 40
}
]
});
});
// 叠加到现有 graphic 上(保留原有 graphic)
const prevElements = Array.isArray(this.currentGraphicElements) ? this.currentGraphicElements : [];
const merged = prevElements.concat(graphics);
this.currentGraphicElements = merged;
this.$nextTick(() => {
if (this.myChart) {
this.myChart.setOption({ graphic: { elements: merged } }, { replaceMerge: ['graphic'] });
}
});
},
// 为每个 x 类目在左右各画一条黑色竖线(仅顶部短竖线)
drawCategoryEdgeLines(xAxisLabels, chartConfig) {
if (!this.myChart || !Array.isArray(xAxisLabels) || xAxisLabels.length === 0) return;
// 确保图表已完全渲染,检查容器尺寸
const chartDom = this.$refs.chartRef;
if (!chartDom || chartDom.offsetWidth === 0 || chartDom.offsetHeight === 0) {
console.warn('图表容器尺寸无效,跳过绘制类目边界线');
return;
}
const yMinPx = this.myChart.convertToPixel({ yAxisIndex: 0 }, chartConfig.yAxis.min);
const yMaxPx = this.myChart.convertToPixel({ yAxisIndex: 0 }, chartConfig.yAxis.max);
if (yMinPx === null || yMaxPx === null || isNaN(yMinPx) || isNaN(yMaxPx)) {
console.warn('convertToPixel 返回无效值,跳过绘制类目边界线');
return;
}
const yTopPx = Math.min(yMinPx, yMaxPx);
const yBottomPx = Math.max(yMinPx, yMaxPx);
// 只在上方画短竖线:长度占绘图区高度的 6%,并限制在 15-60 像素
const plotHeight = Math.abs(yBottomPx - yTopPx);
const stemLen = Math.max(15, Math.min(60, plotHeight * 0.06));
// 仅保留上方的线:整段都在 top 之上
const extraHead = Math.max(6, Math.min(30, plotHeight * 0.03));
const yStemStart = yTopPx - (extraHead + stemLen);
const yStemEnd = yTopPx - 1;
const graphics = [];
const n = xAxisLabels.length;
// 计算每个类目的像素中心
const centers = [];
for (let i = 0; i < n; i++) {
const cx = this.myChart.convertToPixel({ xAxisIndex: 0 }, i);
if (cx != null) centers.push(cx);
}
if (centers.length > 0) {
// 第一个点左侧边界:用前两个中心的半步估计
let firstEdge;
if (centers.length >= 2) {
firstEdge = centers[0] - (centers[1] - centers[0]) / 2;
} else {
// 只有一个点时,给一个合理的固定偏移
firstEdge = centers[0] - 40;
}
graphics.push({
type: 'line',
shape: { x1: firstEdge, y1: yStemStart, x2: firstEdge, y2: yStemEnd },
style: { stroke: '#bbb', lineWidth: 1.5 },
silent: true,
z: 120,
zlevel: 10
});
// 相邻两点之间的中点
for (let i = 0; i < centers.length - 1; i++) {
const mid = (centers[i] + centers[i + 1]) / 2;
graphics.push({
type: 'line',
shape: { x1: mid, y1: yStemStart, x2: mid, y2: yStemEnd },
style: { stroke: '#bbb', lineWidth: 1.5 },
silent: true,
z: 120,
zlevel: 10
});
}
// 右侧最后一个边界线:对称估算半步并绘制(补齐“最后那条线”)
let lastEdge;
if (centers.length >= 2) {
lastEdge = centers[centers.length - 1] + (centers[centers.length - 1] - centers[centers.length - 2]) / 2;
} else {
lastEdge = centers[0] + 40;
}
graphics.push({
type: 'line',
shape: { x1: lastEdge, y1: yStemStart, x2: lastEdge, y2: yStemEnd },
style: { stroke: '#bbb', lineWidth: 1.5 },
silent: true,
z: 120,
zlevel: 10
});
}
const prev = Array.isArray(this.currentGraphicElements) ? this.currentGraphicElements : [];
const merged = prev.concat(graphics);
this.currentGraphicElements = merged;
this.$nextTick(() => {
if (this.myChart) {
this.myChart.setOption({ graphic: { elements: merged } }, { replaceMerge: ['graphic'] });
}
});
},
// 图内根据 x 和 y2 渲染 SVG 纹理(每段使用该 x 在 stackedAreas 中最浅层 y2 对应的 svg),从顶部填充到 y2 深度
async drawTopBandSvg(xAxisLabels, chartConfig, stackedAreas) {
if (!this.myChart || !Array.isArray(xAxisLabels) || !stackedAreas) return;
// 确保图表已完全渲染,检查容器尺寸
const chartDom = this.$refs.chartRef;
if (!chartDom || chartDom.offsetWidth === 0 || chartDom.offsetHeight === 0) {
console.warn('图表容器尺寸无效,跳过绘制顶部SVG纹理');
return;
}
// 计算每个类目的像素中心
const centers = [];
for (let i = 0; i < xAxisLabels.length; i++) {
const cx = this.myChart.convertToPixel({ xAxisIndex: 0 }, i);
if (cx != null) centers.push(cx);
}
if (centers.length === 0) return;
// 计算段边界(第一个左边界 + 各中点)
let firstEdge;
if (centers.length >= 2) {
firstEdge = centers[0] - (centers[1] - centers[0]) / 2;
} else {
firstEdge = centers[0] - 40;
}
const boundaries = [firstEdge];
for (let i = 0; i < centers.length - 1; i++) {
boundaries.push((centers[i] + centers[i + 1]) / 2);
}
// 图内纵向像素范围:从图内顶部到 y2 所在像素
const yMinPx = this.myChart.convertToPixel({ yAxisIndex: 0 }, chartConfig.yAxis.min);
const yMaxPx = this.myChart.convertToPixel({ yAxisIndex: 0 }, chartConfig.yAxis.max);
if (yMinPx == null || yMaxPx == null) return;
const yTopPx = Math.min(yMinPx, yMaxPx);
const plotHeight = Math.abs(yMaxPx - yMinPx);
// 摊平 stackedAreas 以便按 x 查找图层(保留全部层,用于全部渲染)
const allLayers = [];
if (Array.isArray(stackedAreas)) {
for (const l of stackedAreas) if (l) allLayers.push(l);
} else {
for (const layers of Object.values(stackedAreas)) {
if (Array.isArray(layers)) allLayers.push(...layers.filter(Boolean));
}
}
const graphics = [];
// 仅在每口井的最后一列标注层名称
const depthIntervals = this.mockData?.wellData?.depthIntervals || [];
const lastIndexByJh = new Map();
for (let idx = 0; idx < depthIntervals.length; idx++) {
const jhVal = depthIntervals[idx]?.jh;
if (jhVal != null) lastIndexByJh.set(jhVal, idx);
}
for (let i = 0; i < xAxisLabels.length; i++) {
const xLabel = String(xAxisLabels[i]);
const left = boundaries[i];
const right = (i < boundaries.length - 1) ? boundaries[i + 1] : left + (centers[1] ? (centers[1] - centers[0]) : 80);
const width = Math.max(10, right - left);
// 找到包含该 x 的所有层,提取该 x 对应点的 y2;若有重叠(相同 y2)则只保留一次
const y2ToLayer = new Map();
for (const layer of allLayers) {
const pts = Array.isArray(layer?.points) ? layer.points : [];
const p = pts.find(p0 => String(p0?.x) === xLabel);
if (!p) continue;
const y2val = Number(p?.y2 ?? layer?.sjdjsd);
if (Number.isNaN(y2val)) continue;
if (!y2ToLayer.has(y2val)) y2ToLayer.set(y2val, layer);
}
const sortedY2 = Array.from(y2ToLayer.keys()).sort((a, b) => a - b);
let prevBottom = null; // 上一个片段的底(y2),为空则从图内顶部开始
for (const y2val of sortedY2) {
const layer = y2ToLayer.get(y2val);
const yTopDepth = prevBottom == null ? chartConfig?.yAxis?.min : prevBottom;
const yBottomDepth = y2val;
const yPixTop = this.myChart.convertToPixel({ yAxisIndex: 0 }, yTopDepth);
const yPixBottom = this.myChart.convertToPixel({ yAxisIndex: 0 }, yBottomDepth);
if (yPixTop == null || yPixBottom == null) {
prevBottom = y2val;
continue;
}
const rectTop = Math.min(yPixTop, yPixBottom);
const rectBottom = Math.max(yPixTop, yPixBottom);
const y1pix = Math.max(yTopPx, rectTop);
const y2pix = Math.min(Math.max(yMinPx, yMaxPx), rectBottom);
const height = Math.max(0, y2pix - y1pix);
if (height > 0) {
let fill = '#d0d3d8';
if (layer?.svg) {
try {
const img = await this.createSvgImage(layer.svg);
fill = { image: img, repeat: 'repeat' };
} catch (e) { /* ignore */
}
}
graphics.push({
type: 'rect',
shape: { x: left, y: y1pix, width, height },
style: { fill, stroke: 'rgba(0,0,0,0.12)', lineWidth: 1 },
silent: true,
z: -10,
zlevel: 1,
__band: true
});
}
prevBottom = y2val;
}
}
// 合并到当前 graphic 上:先移除旧 band,再追加新的,避免重复渲染
const prev = Array.isArray(this.currentGraphicElements) ? this.currentGraphicElements : [];
const kept = prev.filter(el => !el.__band);
const merged = kept.concat(graphics);
this.currentGraphicElements = merged;
this.$nextTick(() => {
if (this.myChart) {
this.myChart.setOption({ graphic: { elements: merged } }, { replaceMerge: ['graphic'] });
}
});
},
// 导出图表为图片
exportChart() {
if (!this.myChart) {
this.$message.warning('图表尚未加载完成,请稍候再试');
return;
}
try {
// 获取图表图片数据
const dataURL = this.myChart.getDataURL({
type: 'png',
pixelRatio: 2, // 提高图片清晰度
backgroundColor: '#fff'
});
// 创建下载链接
const link = document.createElement('a');
const fileName = this.jh ? `直方图_${this.jh}_${new Date().getTime()}.png` : `直方图_${new Date().getTime()}.png`;
link.href = dataURL;
link.download = fileName;
link.style.display = 'none';
// 触发下载
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
this.$message.success('图表导出成功');
} catch (error) {
console.error('导出图表失败:', error);
this.$message.error('导出图表失败,请稍候再试');
}
}
},
};
</script>
<style lang="scss" scoped>
/* 容器样式优化 */
.chart-container {
width: 100%;
min-height: calc(100vh - 140px);
padding: 0 10px;
margin: 0;
box-sizing: border-box;
display: flex;
flex-direction: column;
position: relative;
overflow: auto;
}
.chart-layout {
display: flex;
flex: 1;
width: 100%;
gap: 5px;
align-items: stretch;
justify-content: flex-start;
overflow: hidden;
}
.chart-left {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 10px;
overflow: hidden;
}
.well-mini-row {
display: flex;
gap: 0;
width: 100%;
min-height: 220px;
padding: 10px 8px 12px;
border-radius: 16px;
background: #fff;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08);
border: 1px solid rgba(15, 23, 42, 0.06);
overflow-x: auto;
overflow-y: hidden;
}
.well-mini-item {
min-width: 120px;
display: flex;
flex-direction: column;
gap: 6px;
padding: 0 6px;
box-sizing: border-box;
border-right: 1px dashed rgba(148, 163, 184, 0.5);
}
.well-mini-item:last-child {
border-right: none;
}
.well-mini-title {
font-size: 12px;
font-weight: 700;
color: #1f2937;
padding: 0 4px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.strata-legend {
width: 240px;
min-width: 240px;
max-height: 100%;
padding: 16px;
border-radius: 16px;
background: #fff;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08);
border: 1px solid rgba(15, 23, 42, 0.06);
display: flex;
flex-direction: column;
gap: 12px;
}
.legend-header {
font-size: 16px;
font-weight: 600;
color: #1f2937;
border-bottom: 1px solid rgba(15, 23, 42, 0.08);
padding-bottom: 8px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.legend-list {
overflow-y: auto;
flex: 1;
display: flex;
flex-direction: column;
gap: 1px;
padding-right: 4px;
}
.legend-item {
display: flex;
align-items: center;
gap: 5px;
padding: 0px 4px;
border-radius: 8px;
transition: background 0.2s ease;
}
.legend-item:hover {
background-color: rgba(59, 130, 246, 0.08);
}
.legend-icon {
width: 40px;
height: 24px;
border-radius: 6px;
border: 1px solid rgba(15, 23, 42, 0.15);
background-color: #d1d5db;
background-size: cover !important;
}
.legend-label {
flex: 1;
font-size: 13px;
font-weight: 500;
color: #1f2937;
}
/* 井号显示样式 */
.well-number-display {
position: absolute;
top: 16px;
left: 16px;
z-index: 5;
background: transparent;
padding: 8px 12px;
display: flex;
align-items: center;
gap: 6px;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
transition: all 0.3s ease;
opacity: 0.9;
}
/* 导出按钮样式 */
::v-deep .export-btn {
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%) !important;
color: #fff !important;
border: none !important;
border-radius: 6px !important;
padding: 6px 14px !important;
font-size: 13px !important;
font-weight: 500 !important;
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3) !important;
transition: all 0.3s ease !important;
}
::v-deep .export-btn:hover:not(:disabled) {
background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%) !important;
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.4) !important;
transform: translateY(-1px) !important;
}
::v-deep .export-btn:active:not(:disabled) {
transform: translateY(0) !important;
box-shadow: 0 2px 6px rgba(59, 130, 246, 0.3) !important;
}
::v-deep .export-btn:disabled {
background: #9ca3af !important;
cursor: not-allowed !important;
opacity: 0.6 !important;
box-shadow: none !important;
}
::v-deep .export-btn .el-icon-download {
color: #fff !important;
}
.well-number-display:hover {
opacity: 1;
}
.well-label {
color: #6b7280;
font-size: 13px;
font-weight: 500;
white-space: nowrap;
}
.well-number {
color: #3b82f6;
font-size: 14px;
font-weight: 600;
}
/* 图表容器样式 */
.chart {
flex: 1;
width: 100%;
max-width: 100%;
height: 100%;
min-height: 400px;
display: block;
border-radius: 16px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
border: 1px solid rgba(255, 255, 255, 0.8);
backdrop-filter: blur(10px);
transition: all 0.3s ease;
}
.chart:hover {
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.15);
/* transform: translateY(-2px); */
}
/* 加载状态样式 */
.loading-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.95);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 10;
border-radius: 16px;
backdrop-filter: blur(10px);
}
.loading-spinner {
width: 48px;
height: 48px;
border: 4px solid #f3f4f6;
border-top: 4px solid #3b82f6;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 16px;
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.2);
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
/* 响应式优化 */
@media (max-width: 768px) {
.chart-container {
padding: 0 10px;
}
.chart {
min-height: 300px;
border-radius: 12px;
}
.export-btn {
padding: 4px 8px;
font-size: 12px;
}
}
/* 深色模式支持 */
@media (prefers-color-scheme: dark) {
.chart {
/* background: linear-gradient(135deg, #1f2937 0%, #111827 100%); */
/* box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4); */
border: 1px solid rgba(255, 255, 255, 0.1);
}
/*
.chart:hover {
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.5);
} */
.loading-overlay {
background: rgba(17, 24, 39, 0.95);
}
}
/* 动画效果 */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.chart-container {
animation: fadeIn 0.6s ease-out;
}
</style>
<template> <template>
<div class="chart-container"> <div class="chart-container">
<div class="chart-layout"> <div class="chart-layout">
<div id="mainzftdj" class="chart" ref="chartRef"></div> <div class="chart-left">
<div id="mainzftdj" class="chart" ref="chartRef"></div>
<div
v-if="wellSegments && wellSegments.length"
class="well-mini-row"
:style="miniRowStyle"
>
<div
v-for="seg in wellSegments"
:key="`${seg.jh}_${seg.startIdx}_${seg.endIdx}`"
class="well-mini-item"
:style="getMiniItemStyle(seg)"
>
<div class="well-mini-title">{{ seg.jh || "-" }}</div>
<WellDrillingTimeMiniChart
:jh="seg.jh"
:rows="getLjRowsByJh(seg.jh)"
:height="miniChartsHeight"
@preview="handleMiniPreview"
/>
</div>
</div>
</div>
<aside v-if="legendItems && legendItems.length" class="strata-legend"> <aside v-if="legendItems && legendItems.length" class="strata-legend">
<div class="legend-header"> <div class="legend-header">
<span>层位图例</span> <span>层位图例</span>
<el-button class="export-btn" @click="exportChart" :disabled="loading || !myChart" title="导出图表为图片" <el-button class="export-btn" @click="exportChart" :disabled="loading || !myChart" title="导出图表为图片"
icon="el-icon-download" size="small"> icon="el-icon-download" size="small">
导出 导出
</el-button> </el-button>
</div> </div>
...@@ -22,16 +44,39 @@ ...@@ -22,16 +44,39 @@
<div class="loading-spinner"></div> <div class="loading-spinner"></div>
<span>加载中...</span> <span>加载中...</span>
</div> </div>
<el-dialog
:visible.sync="previewVisible"
width="80%"
:append-to-body="true"
:close-on-click-modal="true"
class="well-preview-dialog"
>
<span slot="title">
录井曲线预览
<span v-if="previewJh" style="margin-left: 8px; font-weight: 600;">{{ previewJh }}</span>
</span>
<div class="preview-chart-wrapper">
<WellDrillingTimeMiniChart
v-if="previewVisible"
:jh="previewJh"
:rows="previewRows"
:height="420"
:enable-preview="false"
/>
</div>
</el-dialog>
</div> </div>
</template> </template>
<script> <script>
import * as echarts from "echarts"; import * as echarts from "echarts";
import {getdjZft} from "@/api/optimization/initialization"; import { getdjZft } from "@/api/optimization/initialization";
import {text} from "d3"; import { text } from "d3";
import WellDrillingTimeMiniChart from "./WellDrillingTimeMiniChart.vue";
export default { export default {
name: "HistogramGraph", name: "HistogramGraph",
components: { WellDrillingTimeMiniChart },
props: { props: {
jh: { jh: {
type: String, type: String,
...@@ -63,6 +108,13 @@ export default { ...@@ -63,6 +108,13 @@ export default {
lastXAxisLabels: null, lastXAxisLabels: null,
lastDepthIntervals: null, lastDepthIntervals: null,
currentGraphicElements: [], currentGraphicElements: [],
wellSegments: [],
miniChartsHeight: 180,
miniRowPaddingLeft: 0,
miniRowPaddingRight: 0,
previewVisible: false,
previewJh: "",
previewRows: [],
}; };
}, },
computed: { computed: {
...@@ -110,7 +162,13 @@ export default { ...@@ -110,7 +162,13 @@ export default {
}, },
legendPanelWidth() { legendPanelWidth() {
return this.legendItems && this.legendItems.length ? 260 : 0; return this.legendItems && this.legendItems.length ? 260 : 0;
} },
miniRowStyle() {
return {
paddingLeft: `${this.miniRowPaddingLeft}px`,
paddingRight: `${this.miniRowPaddingRight}px`,
};
},
}, },
watch: { watch: {
jh: { jh: {
...@@ -165,10 +223,15 @@ export default { ...@@ -165,10 +223,15 @@ export default {
const mockData = await this.getList(); const mockData = await this.getList();
if (!this.hasValidData(mockData)) { if (!this.hasValidData(mockData)) {
this.wellSegments = [];
this.miniRowPaddingLeft = 0;
this.miniRowPaddingRight = 0;
this.renderEmpty(chartDom); this.renderEmpty(chartDom);
return; return;
} }
this.wellSegments = this.computeWellSegments(mockData?.wellData?.depthIntervals);
await this.renderRealData(mockData); await this.renderRealData(mockData);
} catch (error) { } catch (error) {
...@@ -195,7 +258,11 @@ export default { ...@@ -195,7 +258,11 @@ export default {
const verticalPadding = 40; const verticalPadding = 40;
const heightByViewport = viewportHeight - topOffset - verticalPadding; const heightByViewport = viewportHeight - topOffset - verticalPadding;
const fallbackHeight = this.$el?.clientHeight || viewportHeight; const fallbackHeight = this.$el?.clientHeight || viewportHeight;
const availableHeight = Math.max(360, heightByViewport, fallbackHeight - 20); const miniRowExtra =
this.wellSegments && this.wellSegments.length
? (Number(this.miniChartsHeight) || 180) + 44 // 标题/间距预留
: 0;
const availableHeight = Math.max(360, heightByViewport - miniRowExtra, fallbackHeight - 20 - miniRowExtra);
chartDom.style.width = `${availableWidth}px`; chartDom.style.width = `${availableWidth}px`;
chartDom.style.height = `${availableHeight}px`; chartDom.style.height = `${availableHeight}px`;
chartDom.offsetHeight; // 强制重排 chartDom.offsetHeight; // 强制重排
...@@ -287,6 +354,114 @@ export default { ...@@ -287,6 +354,114 @@ export default {
throw error; throw error;
} }
}, },
getLjRowsByJh(jh) {
if (!jh) return [];
const map = this.mockData?.ljJhMap;
if (map && typeof map === "object" && Array.isArray(map[jh])) return map[jh];
return [];
},
handleMiniPreview(payload) {
const jh = payload && payload.jh ? payload.jh : "";
const rowsFromPayload = payload && Array.isArray(payload.rows) ? payload.rows : [];
this.previewJh = jh;
this.previewRows = rowsFromPayload.length ? rowsFromPayload : this.getLjRowsByJh(jh);
this.previewVisible = true;
},
computeWellSegments(depthIntervals) {
if (!Array.isArray(depthIntervals) || depthIntervals.length === 0) return [];
const segments = [];
let currentJh = null;
let startIdx = 0;
for (let i = 0; i < depthIntervals.length; i++) {
const jh = depthIntervals[i]?.jh ?? depthIntervals[i]?.x;
if (i === 0) {
currentJh = jh;
startIdx = 0;
continue;
}
if (jh !== currentJh) {
const endIdx = i - 1;
const count = Math.max(1, endIdx - startIdx + 1);
segments.push({ jh: currentJh, startIdx, endIdx, count });
currentJh = jh;
startIdx = i;
}
}
const endIdx = depthIntervals.length - 1;
const count = Math.max(1, endIdx - startIdx + 1);
segments.push({ jh: currentJh, startIdx, endIdx, count });
return segments
.filter((s) => s.jh != null && String(s.jh).trim() !== "")
.map((s) => ({ ...s, pxLeft: null, pxWidth: null }));
},
getMiniItemStyle(seg) {
if (seg && Number.isFinite(seg.pxWidth) && seg.pxWidth > 0) {
return {
flex: "0 0 auto",
width: `${Math.max(1, Math.round(seg.pxWidth))}px`,
};
}
return { flex: `${seg?.count || 1} 0 0` };
},
updateMiniSegmentPixels() {
if (!this.myChart) return;
if (!Array.isArray(this.wellSegments) || this.wellSegments.length === 0) {
this.miniRowPaddingLeft = 0;
this.miniRowPaddingRight = 0;
return;
}
const depthIntervals =
this.lastDepthIntervals ||
this.mockData?.wellData?.depthIntervals ||
[];
const n = Array.isArray(depthIntervals) && depthIntervals.length
? depthIntervals.length
: (this.lastXAxisLabels || []).length;
if (!n || n < 1) return;
const chartWidth = this.myChart.getWidth ? this.myChart.getWidth() : null;
const centers = [];
for (let i = 0; i < n; i++) {
const cx = this.myChart.convertToPixel({ xAxisIndex: 0 }, i);
if (cx == null || Number.isNaN(cx)) return;
centers.push(cx);
}
if (!centers.length) return;
const boundaries = [];
let firstEdge;
if (centers.length >= 2) firstEdge = centers[0] - (centers[1] - centers[0]) / 2;
else firstEdge = centers[0] - 40;
boundaries.push(firstEdge);
for (let i = 0; i < centers.length - 1; i++) boundaries.push((centers[i] + centers[i + 1]) / 2);
let lastEdge;
if (centers.length >= 2) lastEdge = centers[centers.length - 1] + (centers[centers.length - 1] - centers[centers.length - 2]) / 2;
else lastEdge = centers[0] + 40;
boundaries.push(lastEdge);
const clamp = (v, min, max) => Math.max(min, Math.min(max, v));
const leftPad = clamp(boundaries[0], 0, chartWidth == null ? boundaries[0] : chartWidth);
const rightPad = chartWidth == null ? 0 : clamp(chartWidth - boundaries[boundaries.length - 1], 0, chartWidth);
this.miniRowPaddingLeft = Math.max(0, Math.round(leftPad));
this.miniRowPaddingRight = Math.max(0, Math.round(rightPad));
this.wellSegments = this.wellSegments.map((seg) => {
const l = boundaries[seg.startIdx];
const r = boundaries[seg.endIdx + 1];
if (l == null || r == null) return { ...seg, pxLeft: null, pxWidth: null };
const l2 = clamp(l, 0, chartWidth == null ? l : chartWidth);
const r2 = clamp(r, 0, chartWidth == null ? r : chartWidth);
const w = Math.max(1, r2 - l2);
return { ...seg, pxLeft: l2, pxWidth: w };
});
},
// 供父组件在切换到本tab时调用 // 供父组件在切换到本tab时调用
async loadData() { async loadData() {
await this.initChart(); await this.initChart();
...@@ -327,6 +502,7 @@ export default { ...@@ -327,6 +502,7 @@ export default {
this.drawCategoryEdgeLines(this.lastXAxisLabels, this.lastChartConfig); this.drawCategoryEdgeLines(this.lastXAxisLabels, this.lastChartConfig);
this.drawTopBandSvg(this.lastXAxisLabels, this.lastChartConfig, this.lastStackedAreas); this.drawTopBandSvg(this.lastXAxisLabels, this.lastChartConfig, this.lastStackedAreas);
} }
this.updateMiniSegmentPixels();
} }
}); });
} }
...@@ -342,7 +518,7 @@ export default { ...@@ -342,7 +518,7 @@ export default {
// 渲染真实数据 // 渲染真实数据
async renderRealData(mockData) { async renderRealData(mockData) {
try { try {
const {wellData} = mockData; const { wellData } = mockData;
console.log(wellData, 'wellData'); console.log(wellData, 'wellData');
const xAxisLabels = wellData.depthLine.map((point) => point.x); const xAxisLabels = wellData.depthLine.map((point) => point.x);
try { try {
...@@ -382,6 +558,7 @@ export default { ...@@ -382,6 +558,7 @@ export default {
this.drawJhSeparators(this.lastDepthIntervals, this.lastXAxisLabels, this.lastChartConfig); this.drawJhSeparators(this.lastDepthIntervals, this.lastXAxisLabels, this.lastChartConfig);
this.drawCategoryEdgeLines(this.lastXAxisLabels, this.lastChartConfig); this.drawCategoryEdgeLines(this.lastXAxisLabels, this.lastChartConfig);
this.drawTopBandSvg(this.lastXAxisLabels, this.lastChartConfig, this.lastStackedAreas); this.drawTopBandSvg(this.lastXAxisLabels, this.lastChartConfig, this.lastStackedAreas);
this.updateMiniSegmentPixels();
} else { } else {
// 如果尺寸还未确定,延迟重试 // 如果尺寸还未确定,延迟重试
setTimeout(() => { setTimeout(() => {
...@@ -395,6 +572,7 @@ export default { ...@@ -395,6 +572,7 @@ export default {
this.drawJhSeparators(this.lastDepthIntervals, this.lastXAxisLabels, this.lastChartConfig); this.drawJhSeparators(this.lastDepthIntervals, this.lastXAxisLabels, this.lastChartConfig);
this.drawCategoryEdgeLines(this.lastXAxisLabels, this.lastChartConfig); this.drawCategoryEdgeLines(this.lastXAxisLabels, this.lastChartConfig);
this.drawTopBandSvg(this.lastXAxisLabels, this.lastChartConfig, this.lastStackedAreas); this.drawTopBandSvg(this.lastXAxisLabels, this.lastChartConfig, this.lastStackedAreas);
this.updateMiniSegmentPixels();
} }
}, 150); }, 150);
} }
...@@ -410,20 +588,20 @@ export default { ...@@ -410,20 +588,20 @@ export default {
getDefaultChartOption() { getDefaultChartOption() {
const colors = this.colorScheme; const colors = this.colorScheme;
return { return {
title: {text: "", subtext: ""}, title: { text: "", subtext: "" },
aria: {enabled: false}, aria: { enabled: false },
backgroundColor: colors.background, backgroundColor: colors.background,
tooltip: { tooltip: {
trigger: "axis", trigger: "axis",
axisPointer: { axisPointer: {
type: "cross", type: "cross",
label: {backgroundColor: colors.primary, show:false}, label: { backgroundColor: colors.primary, show: false },
crossStyle: {color: colors.border} crossStyle: { color: colors.border }
}, },
backgroundColor: 'rgba(255,255,255,0.95)', backgroundColor: 'rgba(255,255,255,0.95)',
borderColor: colors.border, borderColor: colors.border,
borderWidth: 1, borderWidth: 1,
textStyle: {color: colors.text}, textStyle: { color: colors.text },
extraCssText: 'box-shadow: 0 4px 12px rgba(0,0,0,0.15); border-radius: 8px;', extraCssText: 'box-shadow: 0 4px 12px rgba(0,0,0,0.15); border-radius: 8px;',
formatter: (params) => { formatter: (params) => {
if (!params || params.length === 0) return ''; if (!params || params.length === 0) return '';
...@@ -494,11 +672,11 @@ export default { ...@@ -494,11 +672,11 @@ export default {
stackedAreas stackedAreas
.filter(area => area.svg !== null) .filter(area => area.svg !== null)
.map(async (area) => { .map(async (area) => {
let areaStyle = {opacity: 0.6}; let areaStyle = { opacity: 0.6 };
if (area.svg) { if (area.svg) {
try { try {
const svgImage = await this.createSvgImage(area.svg); const svgImage = await this.createSvgImage(area.svg);
areaStyle.color = {image: svgImage, repeat: "repeat"}; areaStyle.color = { image: svgImage, repeat: "repeat" };
} catch (error) { } catch (error) {
console.error('SVG转换失败:', error); console.error('SVG转换失败:', error);
} }
...@@ -512,7 +690,7 @@ export default { ...@@ -512,7 +690,7 @@ export default {
symbol: "none", symbol: "none",
showSymbol: false, showSymbol: false,
areaStyle: areaStyle, areaStyle: areaStyle,
lineStyle: {width: 0}, lineStyle: { width: 0 },
data: area.points.map((point) => (point && point.y2 != null) ? point.y2 : point.y), data: area.points.map((point) => (point && point.y2 != null) ? point.y2 : point.y),
}; };
}) })
...@@ -529,7 +707,7 @@ export default { ...@@ -529,7 +707,7 @@ export default {
if (!xToIndices.has(key)) xToIndices.set(key, []); if (!xToIndices.has(key)) xToIndices.set(key, []);
xToIndices.get(key).push(idx); xToIndices.get(key).push(idx);
}); });
const buildNullArray = (len) => Array.from({length: len}, () => null); const buildNullArray = (len) => Array.from({ length: len }, () => null);
console.log(buildNullArray, 'buildNullArray'); console.log(buildNullArray, 'buildNullArray');
const results = []; const results = [];
for (const [jh, layers] of Object.entries(stackedAreas)) { for (const [jh, layers] of Object.entries(stackedAreas)) {
...@@ -541,7 +719,7 @@ export default { ...@@ -541,7 +719,7 @@ export default {
for (const p of (area?.points || [])) { for (const p of (area?.points || [])) {
const arr = xToIndices.get(String(p?.x)) || []; const arr = xToIndices.get(String(p?.x)) || [];
if (arr.length === 0) { if (arr.length === 0) {
missingInXAxis.push({x: p?.x, jh, areaName: area?.name}); missingInXAxis.push({ x: p?.x, jh, areaName: area?.name });
} else { } else {
indicesForJh.push(...arr); indicesForJh.push(...arr);
} }
...@@ -551,11 +729,11 @@ export default { ...@@ -551,11 +729,11 @@ export default {
const endIdx = Math.max(...indicesForJh); const endIdx = Math.max(...indicesForJh);
for (const area of (layers || [])) { for (const area of (layers || [])) {
if (area == null) continue; if (area == null) continue;
let areaStyle = {opacity: 0.6}; let areaStyle = { opacity: 0.6 };
if (area.svg) { if (area.svg) {
try { try {
const svgImage = await this.createSvgImage(area.svg); const svgImage = await this.createSvgImage(area.svg);
areaStyle.color = {image: svgImage, repeat: "repeat"}; areaStyle.color = { image: svgImage, repeat: "repeat" };
} catch (error) { } catch (error) {
console.error('SVG转换失败:', error); console.error('SVG转换失败:', error);
} }
...@@ -575,7 +753,7 @@ export default { ...@@ -575,7 +753,7 @@ export default {
} }
}); });
if (!placed) { if (!placed) {
outOfRangePoints.push({x: p?.x, cands, jh, areaName: area?.name, startIdx, endIdx}); outOfRangePoints.push({ x: p?.x, cands, jh, areaName: area?.name, startIdx, endIdx });
} }
} }
results.push({ results.push({
...@@ -587,7 +765,7 @@ export default { ...@@ -587,7 +765,7 @@ export default {
symbol: "none", symbol: "none",
showSymbol: false, showSymbol: false,
areaStyle: areaStyle, areaStyle: areaStyle,
lineStyle: {width: 0}, lineStyle: { width: 0 },
data data
}); });
// 仅在有异常时输出调试信息,方便定位 x 匹配问题 // 仅在有异常时输出调试信息,方便定位 x 匹配问题
...@@ -599,7 +777,7 @@ export default { ...@@ -599,7 +777,7 @@ export default {
} }
if (outOfRangePoints.length > 0) { if (outOfRangePoints.length > 0) {
console.warn('x 命中但不在井段范围内:', outOfRangePoints); console.warn('x 命中但不在井段范围内:', outOfRangePoints);
console.info('井段范围:', {jh, startIdx, endIdx}); console.info('井段范围:', { jh, startIdx, endIdx });
} }
console.groupEnd && console.groupEnd(); console.groupEnd && console.groupEnd();
} }
...@@ -630,9 +808,9 @@ export default { ...@@ -630,9 +808,9 @@ export default {
axisTick: { axisTick: {
alignWithLabel: true, alignWithLabel: true,
length: 6, length: 6,
lineStyle: {color: colors.border} lineStyle: { color: colors.border }
}, },
splitLine: {show: false}, splitLine: { show: false },
axisLine: { axisLine: {
show: true, show: true,
lineStyle: { lineStyle: {
...@@ -690,7 +868,7 @@ export default { ...@@ -690,7 +868,7 @@ export default {
}, },
axisTick: { axisTick: {
show: true, show: true,
lineStyle: {color: colors.border} lineStyle: { color: colors.border }
} }
}, },
{ {
...@@ -712,7 +890,7 @@ export default { ...@@ -712,7 +890,7 @@ export default {
color: colors.text, color: colors.text,
fontWeight: 500 fontWeight: 500
}, },
splitLine: {show: false}, splitLine: { show: false },
axisLine: { axisLine: {
show: true, show: true,
lineStyle: { lineStyle: {
...@@ -722,7 +900,7 @@ export default { ...@@ -722,7 +900,7 @@ export default {
}, },
axisTick: { axisTick: {
show: true, show: true,
lineStyle: {color: colors.border} lineStyle: { color: colors.border }
} }
}, },
]; ];
...@@ -820,8 +998,8 @@ export default { ...@@ -820,8 +998,8 @@ export default {
x2: 0, x2: 0,
y2: 1, y2: 1,
colorStops: [ colorStops: [
{offset: 0, color: colors.gradient.start}, { offset: 0, color: colors.gradient.start },
{offset: 1, color: colors.gradient.end} { offset: 1, color: colors.gradient.end }
] ]
}, },
borderRadius: [4, 4, 0, 0], borderRadius: [4, 4, 0, 0],
...@@ -838,8 +1016,8 @@ export default { ...@@ -838,8 +1016,8 @@ export default {
x2: 0, x2: 0,
y2: 1, y2: 1,
colorStops: [ colorStops: [
{offset: 0, color: this.adjustColor(colors.gradient.start, 1.2)}, { offset: 0, color: this.adjustColor(colors.gradient.start, 1.2) },
{offset: 1, color: this.adjustColor(colors.gradient.end, 1.2)} { offset: 1, color: this.adjustColor(colors.gradient.end, 1.2) }
] ]
}, },
shadowBlur: 12, shadowBlur: 12,
...@@ -863,7 +1041,7 @@ export default { ...@@ -863,7 +1041,7 @@ export default {
color: 'transparent', color: 'transparent',
borderColor: 'transparent' borderColor: 'transparent'
}, },
emphasis: {itemStyle: {color: 'transparent', borderColor: 'transparent'}}, emphasis: { itemStyle: { color: 'transparent', borderColor: 'transparent' } },
label: { label: {
show: false, show: false,
position: 'insideBottom', position: 'insideBottom',
...@@ -956,7 +1134,7 @@ export default { ...@@ -956,7 +1134,7 @@ export default {
// 创建SVG图片 // 创建SVG图片
createSvgImage(svgString) { createSvgImage(svgString) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const svgBlob = new Blob([svgString], {type: 'image/svg+xml;charset=utf-8'}); const svgBlob = new Blob([svgString], { type: 'image/svg+xml;charset=utf-8' });
const svgUrl = URL.createObjectURL(svgBlob); const svgUrl = URL.createObjectURL(svgBlob);
const img = new Image(); const img = new Image();
img.onload = () => { img.onload = () => {
...@@ -1007,7 +1185,7 @@ export default { ...@@ -1007,7 +1185,7 @@ export default {
this.currentGraphicElements = elements; this.currentGraphicElements = elements;
this.$nextTick(() => { this.$nextTick(() => {
if (this.myChart) { if (this.myChart) {
this.myChart.setOption({graphic: {elements}}, {replaceMerge: ['graphic']}); this.myChart.setOption({ graphic: { elements } }, { replaceMerge: ['graphic'] });
} }
}); });
}, },
...@@ -1025,7 +1203,7 @@ export default { ...@@ -1025,7 +1203,7 @@ export default {
for (let i = 0; i < depthIntervals.length; i++) { for (let i = 0; i < depthIntervals.length; i++) {
// 直接使用在 depthIntervals 中的顺序索引作为 x 轴位置, // 直接使用在 depthIntervals 中的顺序索引作为 x 轴位置,
// 以避免相同 x 标签造成的覆盖与错位 // 以避免相同 x 标签造成的覆盖与错位
const {jh} = depthIntervals[i]; const { jh } = depthIntervals[i];
const idx = i; const idx = i;
if (!started) { if (!started) {
currentJh = jh; currentJh = jh;
...@@ -1063,8 +1241,8 @@ export default { ...@@ -1063,8 +1241,8 @@ export default {
return; return;
} }
// 垂直范围(像素)——考虑 y 轴 inverse,取像素最小为顶部、最大为底部 // 垂直范围(像素)——考虑 y 轴 inverse,取像素最小为顶部、最大为底部
const yMinPx = this.myChart.convertToPixel({yAxisIndex: 0}, chartConfig.yAxis.min); const yMinPx = this.myChart.convertToPixel({ yAxisIndex: 0 }, chartConfig.yAxis.min);
const yMaxPx = this.myChart.convertToPixel({yAxisIndex: 0}, chartConfig.yAxis.max); const yMaxPx = this.myChart.convertToPixel({ yAxisIndex: 0 }, chartConfig.yAxis.max);
if (yMinPx === null || yMaxPx === null || isNaN(yMinPx) || isNaN(yMaxPx)) { if (yMinPx === null || yMaxPx === null || isNaN(yMinPx) || isNaN(yMaxPx)) {
console.warn('convertToPixel 返回无效值,跳过绘制井号分隔符'); console.warn('convertToPixel 返回无效值,跳过绘制井号分隔符');
return; return;
...@@ -1076,8 +1254,8 @@ export default { ...@@ -1076,8 +1254,8 @@ export default {
for (let i = 0; i < segments.length - 1; i++) { for (let i = 0; i < segments.length - 1; i++) {
const leftEnd = segments[i].endIdx; const leftEnd = segments[i].endIdx;
const rightStart = segments[i + 1].startIdx; const rightStart = segments[i + 1].startIdx;
const pxLeft = this.myChart.convertToPixel({xAxisIndex: 0}, leftEnd); const pxLeft = this.myChart.convertToPixel({ xAxisIndex: 0 }, leftEnd);
const pxRight = this.myChart.convertToPixel({xAxisIndex: 0}, rightStart); const pxRight = this.myChart.convertToPixel({ xAxisIndex: 0 }, rightStart);
if (pxLeft === null || pxRight === null) { if (pxLeft === null || pxRight === null) {
console.warn('convertToPixel 返回 null 值,跳过绘制边界虚线'); console.warn('convertToPixel 返回 null 值,跳过绘制边界虚线');
continue; continue;
...@@ -1085,11 +1263,12 @@ export default { ...@@ -1085,11 +1263,12 @@ export default {
const midX = (pxLeft + pxRight) / 2; const midX = (pxLeft + pxRight) / 2;
graphics.push({ graphics.push({
type: 'line', type: 'line',
shape: {x1: midX, y1: yTopPx, x2: midX, y2: yBottomPx}, shape: { x1: midX, y1: yTopPx, x2: midX, y2: yBottomPx },
style: {stroke: '#bbb', lineDash: [8, 6], lineWidth: 1.5}, style: { stroke: '#bbb', lineDash: [8, 6], lineWidth: 1.5 },
silent: true, silent: true,
z: 100, z: 100,
zlevel: 10 zlevel: 10,
__jhSep: true
}); });
} }
...@@ -1098,8 +1277,8 @@ export default { ...@@ -1098,8 +1277,8 @@ export default {
let rowShift = 0; // 逐行上移避免覆盖 let rowShift = 0; // 逐行上移避免覆盖
segments.forEach((seg, idx) => { segments.forEach((seg, idx) => {
if (!seg.jh) return; if (!seg.jh) return;
const pxStart = this.myChart.convertToPixel({xAxisIndex: 0}, seg.startIdx); const pxStart = this.myChart.convertToPixel({ xAxisIndex: 0 }, seg.startIdx);
const pxEnd = this.myChart.convertToPixel({xAxisIndex: 0}, seg.endIdx); const pxEnd = this.myChart.convertToPixel({ xAxisIndex: 0 }, seg.endIdx);
if (pxStart === null || pxEnd === null) { if (pxStart === null || pxEnd === null) {
console.warn('convertToPixel 返回 null 值,跳过绘制井号标签'); console.warn('convertToPixel 返回 null 值,跳过绘制井号标签');
return; return;
...@@ -1131,10 +1310,11 @@ export default { ...@@ -1131,10 +1310,11 @@ export default {
z: 101, z: 101,
zlevel: 10, zlevel: 10,
silent: true, silent: true,
__jhSep: true,
children: [ children: [
{ {
type: 'rect', type: 'rect',
shape: {x: 0, y: 0, width: labelWidth, height: labelHeight, r: 8}, shape: { x: 0, y: 0, width: labelWidth, height: labelHeight, r: 8 },
style: { style: {
fill: '#2563eb', fill: '#2563eb',
stroke: '#1e40af', stroke: '#1e40af',
...@@ -1152,7 +1332,7 @@ export default { ...@@ -1152,7 +1332,7 @@ export default {
[labelWidth / 2, labelHeight + pointerSize] [labelWidth / 2, labelHeight + pointerSize]
] ]
}, },
style: {fill: '#2563eb', stroke: '#1e40af'} style: { fill: '#2563eb', stroke: '#1e40af' }
}, },
{ {
type: 'text', type: 'text',
...@@ -1174,11 +1354,12 @@ export default { ...@@ -1174,11 +1354,12 @@ export default {
// 叠加到现有 graphic 上(保留原有 graphic) // 叠加到现有 graphic 上(保留原有 graphic)
const prevElements = Array.isArray(this.currentGraphicElements) ? this.currentGraphicElements : []; const prevElements = Array.isArray(this.currentGraphicElements) ? this.currentGraphicElements : [];
const merged = prevElements.concat(graphics); const kept = prevElements.filter(el => !el.__jhSep);
const merged = kept.concat(graphics);
this.currentGraphicElements = merged; this.currentGraphicElements = merged;
this.$nextTick(() => { this.$nextTick(() => {
if (this.myChart) { if (this.myChart) {
this.myChart.setOption({graphic: {elements: merged}}, {replaceMerge: ['graphic']}); this.myChart.setOption({ graphic: { elements: merged } }, { replaceMerge: ['graphic'] });
} }
}); });
}, },
...@@ -1194,8 +1375,8 @@ export default { ...@@ -1194,8 +1375,8 @@ export default {
return; return;
} }
const yMinPx = this.myChart.convertToPixel({yAxisIndex: 0}, chartConfig.yAxis.min); const yMinPx = this.myChart.convertToPixel({ yAxisIndex: 0 }, chartConfig.yAxis.min);
const yMaxPx = this.myChart.convertToPixel({yAxisIndex: 0}, chartConfig.yAxis.max); const yMaxPx = this.myChart.convertToPixel({ yAxisIndex: 0 }, chartConfig.yAxis.max);
if (yMinPx === null || yMaxPx === null || isNaN(yMinPx) || isNaN(yMaxPx)) { if (yMinPx === null || yMaxPx === null || isNaN(yMinPx) || isNaN(yMaxPx)) {
console.warn('convertToPixel 返回无效值,跳过绘制类目边界线'); console.warn('convertToPixel 返回无效值,跳过绘制类目边界线');
return; return;
...@@ -1215,7 +1396,7 @@ export default { ...@@ -1215,7 +1396,7 @@ export default {
// 计算每个类目的像素中心 // 计算每个类目的像素中心
const centers = []; const centers = [];
for (let i = 0; i < n; i++) { for (let i = 0; i < n; i++) {
const cx = this.myChart.convertToPixel({xAxisIndex: 0}, i); const cx = this.myChart.convertToPixel({ xAxisIndex: 0 }, i);
if (cx != null) centers.push(cx); if (cx != null) centers.push(cx);
} }
if (centers.length > 0) { if (centers.length > 0) {
...@@ -1229,22 +1410,24 @@ export default { ...@@ -1229,22 +1410,24 @@ export default {
} }
graphics.push({ graphics.push({
type: 'line', type: 'line',
shape: {x1: firstEdge, y1: yStemStart, x2: firstEdge, y2: yStemEnd}, shape: { x1: firstEdge, y1: yStemStart, x2: firstEdge, y2: yStemEnd },
style: {stroke: '#bbb', lineWidth: 1.5}, style: { stroke: '#bbb', lineWidth: 1.5 },
silent: true, silent: true,
z: 120, z: 120,
zlevel: 10 zlevel: 10,
__catEdge: true
}); });
// 相邻两点之间的中点 // 相邻两点之间的中点
for (let i = 0; i < centers.length - 1; i++) { for (let i = 0; i < centers.length - 1; i++) {
const mid = (centers[i] + centers[i + 1]) / 2; const mid = (centers[i] + centers[i + 1]) / 2;
graphics.push({ graphics.push({
type: 'line', type: 'line',
shape: {x1: mid, y1: yStemStart, x2: mid, y2: yStemEnd}, shape: { x1: mid, y1: yStemStart, x2: mid, y2: yStemEnd },
style: {stroke: '#bbb', lineWidth: 1.5}, style: { stroke: '#bbb', lineWidth: 1.5 },
silent: true, silent: true,
z: 120, z: 120,
zlevel: 10 zlevel: 10,
__catEdge: true
}); });
} }
...@@ -1257,21 +1440,23 @@ export default { ...@@ -1257,21 +1440,23 @@ export default {
} }
graphics.push({ graphics.push({
type: 'line', type: 'line',
shape: {x1: lastEdge, y1: yStemStart, x2: lastEdge, y2: yStemEnd}, shape: { x1: lastEdge, y1: yStemStart, x2: lastEdge, y2: yStemEnd },
style: {stroke: '#bbb', lineWidth: 1.5}, style: { stroke: '#bbb', lineWidth: 1.5 },
silent: true, silent: true,
z: 120, z: 120,
zlevel: 10 zlevel: 10,
__catEdge: true
}); });
} }
const prev = Array.isArray(this.currentGraphicElements) ? this.currentGraphicElements : []; const prev = Array.isArray(this.currentGraphicElements) ? this.currentGraphicElements : [];
const merged = prev.concat(graphics); const kept = prev.filter(el => !el.__catEdge);
const merged = kept.concat(graphics);
this.currentGraphicElements = merged; this.currentGraphicElements = merged;
this.$nextTick(() => { this.$nextTick(() => {
if (this.myChart) { if (this.myChart) {
this.myChart.setOption({graphic: {elements: merged}}, {replaceMerge: ['graphic']}); this.myChart.setOption({ graphic: { elements: merged } }, { replaceMerge: ['graphic'] });
} }
}); });
}, },
...@@ -1290,7 +1475,7 @@ export default { ...@@ -1290,7 +1475,7 @@ export default {
// 计算每个类目的像素中心 // 计算每个类目的像素中心
const centers = []; const centers = [];
for (let i = 0; i < xAxisLabels.length; i++) { for (let i = 0; i < xAxisLabels.length; i++) {
const cx = this.myChart.convertToPixel({xAxisIndex: 0}, i); const cx = this.myChart.convertToPixel({ xAxisIndex: 0 }, i);
if (cx != null) centers.push(cx); if (cx != null) centers.push(cx);
} }
if (centers.length === 0) return; if (centers.length === 0) return;
...@@ -1308,8 +1493,8 @@ export default { ...@@ -1308,8 +1493,8 @@ export default {
} }
// 图内纵向像素范围:从图内顶部到 y2 所在像素 // 图内纵向像素范围:从图内顶部到 y2 所在像素
const yMinPx = this.myChart.convertToPixel({yAxisIndex: 0}, chartConfig.yAxis.min); const yMinPx = this.myChart.convertToPixel({ yAxisIndex: 0 }, chartConfig.yAxis.min);
const yMaxPx = this.myChart.convertToPixel({yAxisIndex: 0}, chartConfig.yAxis.max); const yMaxPx = this.myChart.convertToPixel({ yAxisIndex: 0 }, chartConfig.yAxis.max);
if (yMinPx == null || yMaxPx == null) return; if (yMinPx == null || yMaxPx == null) return;
const yTopPx = Math.min(yMinPx, yMaxPx); const yTopPx = Math.min(yMinPx, yMaxPx);
const plotHeight = Math.abs(yMaxPx - yMinPx); const plotHeight = Math.abs(yMaxPx - yMinPx);
...@@ -1357,8 +1542,8 @@ export default { ...@@ -1357,8 +1542,8 @@ export default {
const yTopDepth = prevBottom == null ? chartConfig?.yAxis?.min : prevBottom; const yTopDepth = prevBottom == null ? chartConfig?.yAxis?.min : prevBottom;
const yBottomDepth = y2val; const yBottomDepth = y2val;
const yPixTop = this.myChart.convertToPixel({yAxisIndex: 0}, yTopDepth); const yPixTop = this.myChart.convertToPixel({ yAxisIndex: 0 }, yTopDepth);
const yPixBottom = this.myChart.convertToPixel({yAxisIndex: 0}, yBottomDepth); const yPixBottom = this.myChart.convertToPixel({ yAxisIndex: 0 }, yBottomDepth);
if (yPixTop == null || yPixBottom == null) { if (yPixTop == null || yPixBottom == null) {
prevBottom = y2val; prevBottom = y2val;
continue; continue;
...@@ -1374,14 +1559,14 @@ export default { ...@@ -1374,14 +1559,14 @@ export default {
if (layer?.svg) { if (layer?.svg) {
try { try {
const img = await this.createSvgImage(layer.svg); const img = await this.createSvgImage(layer.svg);
fill = {image: img, repeat: 'repeat'}; fill = { image: img, repeat: 'repeat' };
} catch (e) { /* ignore */ } catch (e) { /* ignore */
} }
} }
graphics.push({ graphics.push({
type: 'rect', type: 'rect',
shape: {x: left, y: y1pix, width, height}, shape: { x: left, y: y1pix, width, height },
style: {fill, stroke: 'rgba(0,0,0,0.12)', lineWidth: 1}, style: { fill, stroke: 'rgba(0,0,0,0.12)', lineWidth: 1 },
silent: true, silent: true,
z: -10, z: -10,
zlevel: 1, zlevel: 1,
...@@ -1399,7 +1584,7 @@ export default { ...@@ -1399,7 +1584,7 @@ export default {
this.currentGraphicElements = merged; this.currentGraphicElements = merged;
this.$nextTick(() => { this.$nextTick(() => {
if (this.myChart) { if (this.myChart) {
this.myChart.setOption({graphic: {elements: merged}}, {replaceMerge: ['graphic']}); this.myChart.setOption({ graphic: { elements: merged } }, { replaceMerge: ['graphic'] });
} }
}); });
}, },
...@@ -1466,6 +1651,62 @@ export default { ...@@ -1466,6 +1651,62 @@ export default {
overflow: hidden; overflow: hidden;
} }
.chart-left {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 10px;
overflow: hidden;
}
.well-mini-row {
display: flex;
gap: 0;
width: 100%;
min-height: 220px;
padding: 10px 0 12px;
border-radius: 16px;
background: #fff;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08);
border: 1px solid rgba(15, 23, 42, 0.06);
overflow-x: auto;
overflow-y: hidden;
}
.well-mini-item {
min-width: 120px;
display: flex;
flex-direction: column;
gap: 6px;
padding: 0 6px;
box-sizing: border-box;
border-right: 1px dashed rgba(148, 163, 184, 0.5);
}
.well-mini-item:last-child {
border-right: none;
}
.well-mini-title {
font-size: 12px;
font-weight: 700;
color: #1f2937;
padding: 0 4px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.well-preview-dialog ::v-deep .el-dialog__body {
padding: 10px 18px 20px;
}
.preview-chart-wrapper {
width: 100%;
height: 100%;
}
.strata-legend { .strata-legend {
width: 240px; width: 240px;
min-width: 240px; min-width: 240px;
......
<template>
<div class="multi-well-structure" v-loading="isLoading" element-loading-text="加载中...">
<el-row :gutter="10">
<el-col v-for="well in wellList" :key="well" :xs="24" :sm="24" :md="12" :lg="12" :xl="12" class="well-col">
<div class="jsjgt-wrapper">
<div class="jsjgt-title">
{{ well }}
</div>
<div class="jsjgt-body">
<svg :class="['svg2', svgClass(well)]"></svg>
<div v-if="!loadedMap[well]" class="jsjgt-empty">
{{ loadingMap[well] ? '加载中...' : '暂无数据' }}
</div>
</div>
</div>
</el-col>
</el-row>
</div>
</template>
<script>
import * as d3 from 'd3';
import { getJsjgt } from '@/api/system/jsdb';
export default {
name: 'MultiWellStructure',
props: {
// 主井
jh: {
type: String,
default: ''
},
// 邻井(逗号分隔)
jhs: {
type: String,
default: ''
},
// 是否处于当前激活tab(用于懒加载)
active: {
type: Boolean,
default: false
}
},
data() {
return {
width: 600,
height: 800,
marginTop: 20,
marginRight: 30,
marginBottom: 30,
marginLeft: 40,
loadingMap: {},
loadedMap: {}
};
},
computed: {
wellList() {
const wells = [];
if (this.jh) {
wells.push(this.jh);
}
if (this.jhs) {
this.jhs
.split(',')
.map((w) => w && w.trim())
.filter(Boolean)
.forEach((w) => {
if (!wells.includes(w)) {
wells.push(w);
}
});
}
return wells;
},
/** 是否有接口尚未请求完成,用于显示遮罩 */
isLoading() {
return this.wellList.length > 0 && this.wellList.some((w) => this.loadingMap[w]);
}
},
watch: {
active: {
handler(val) {
if (val) {
// 每次切到该tab都强制重新请求并重画
this.reload();
}
},
immediate: true
},
wellList() {
// 井列表变化时:如果当前tab激活,立即刷新;否则等激活时再加载
if (this.active) {
this.reload();
}
}
},
methods: {
svgClass(well) {
return `svg2-${this.normalizeWell(well)}`;
},
normalizeWell(well) {
return String(well).replace(/[^a-zA-Z0-9_-]/g, '-');
},
svgSelector(well) {
return `.${this.svgClass(well)}`;
},
// 供父组件在tab点击时主动触发
reload() {
if (!this.active) return;
this.loadAll();
},
loadAll() {
// 清空旧图
(this._prevWells || []).forEach((w) => {
this.clearSvgForWell(w);
});
this._prevWells = [...this.wellList];
this.wellList.forEach((well) => {
this.loadForWell(well);
});
},
loadForWell(well) {
if (!well) return;
this.$set(this.loadingMap, well, true);
this.$set(this.loadedMap, well, false);
this.clearSvgForWell(well);
getJsjgt({ jh: well })
.then((res) => {
if (res && res.code === 200 && res.jsJgmap) {
this.drawWellStructure(well, res.jsJgmap);
this.$set(this.loadedMap, well, true);
} else {
this.clearSvgForWell(well);
}
})
.catch(() => {
this.clearSvgForWell(well);
})
.finally(() => {
this.$set(this.loadingMap, well, false);
});
},
clearSvgForWell(well) {
const selector = this.svgSelector(well);
d3.select(selector).selectAll('*').remove();
},
drawWellStructure(well, res) {
const selector = this.svgSelector(well);
const svg2 = d3
.select(selector)
.attr('viewBox', [0, 0, this.width, this.height])
.attr('width', '100%')
.attr('height', '100%')
.attr('preserveAspectRatio', 'xMidYMid meet')
.attr(
'style',
'width: 100%; height: 100%; font: 10px sans-serif;'
)
.style('-webkit-tap-highlight-color', 'transparent')
.style('overflow', 'visible');
svg2.selectAll('*').remove();
const maxX = Number(res.maxX);
const maxY = Number(res.maxY);
const x = d3.scaleLinear([0, maxX], [0, this.width]);
const y = d3.scaleLinear(
[maxY, 0],
[this.height - this.marginBottom, this.marginTop]
);
const line = d3
.line()
.x((d) => x(d.value))
.y((d) => y(d.depth));
svg2
.append('rect')
.attr('width', this.width)
.attr('height', this.height)
.style('fill', 'transparent');
svg2
.append('g')
.attr('transform', `translate(0,${this.marginTop})`)
.call(d3.axisTop(x).ticks(this.width / 100).tickSizeOuter(0));
const tickCount = 50;
const tickInterval = Math.max(1, Math.floor(maxY / tickCount));
const tickValues = d3.range(0, maxY + 1, tickInterval);
const tickFormat = (value) => {
return value % 5 === 0 ? value : '';
};
const yAxis = d3
.axisRight(y)
.tickValues(tickValues)
.tickFormat(tickFormat);
svg2
.append('g')
.attr('transform', 'translate(10,0)')
.call(yAxis);
svg2.selectAll('.tick text').style('fill', '#000');
svg2.selectAll('.tick line').style('stroke', '#000');
svg2.selectAll('.domain').style('stroke', '#000');
const gradient = svg2
.append('defs')
.append('linearGradient')
.attr('id', `gray-to-white-to-gray-${this.normalizeWell(well)}`)
.attr('x1', '0%')
.attr('y1', '0%')
.attr('x2', '100%')
.attr('y2', '0%');
gradient
.append('stop')
.attr('offset', '0%')
.attr('stop-color', 'gray')
.attr('stop-opacity', 1);
gradient
.append('stop')
.attr('offset', '50%')
.attr('stop-color', 'white')
.attr('stop-opacity', 1);
gradient
.append('stop')
.attr('offset', '100%')
.attr('stop-color', 'gray')
.attr('stop-opacity', 1);
const area = d3
.area()
.x0((d) => x(d.value1))
.x1((d) => x(d.value2))
.y0((d) => y(d.depth1))
.y1((d) => y(d.depth2));
// 深色填充区域
if (Array.isArray(res.svg2ConstructFillDark)) {
for (let k = 0; k < res.svg2ConstructFillDark.length; k++) {
const item = res.svg2ConstructFillDark[k];
if (!item || !item.fill) continue;
svg2
.append('path')
.attr('transform', 'translate(0,0)')
.datum(item.fill)
.attr('class', 'area-dark')
.attr('d', area)
.attr('fill', 'rgb(60,60,60)');
}
}
if (Array.isArray(res.svg2ConstructFillGrey)) {
for (let k = 0; k < res.svg2ConstructFillGrey.length; k++) {
const item = res.svg2ConstructFillGrey[k];
if (!item || !item.fill) continue;
const path = svg2
.append('path')
.attr('transform', 'translate(0,0)')
.datum(item.fill)
.attr('class', 'area')
.attr('d', area);
if (item.gradient === true) {
path.style(
'fill',
`url(#gray-to-white-to-gray-${this.normalizeWell(well)})`
);
} else {
path.attr('fill', 'silver');
}
}
}
const triangleLeft = { x1: 0, y1: 0, x2: -10, y2: 0, x3: 0, y3: -10 };
const triangleRight = { x1: 0, y1: 0, x2: 0, y2: -10, x3: 10, y3: 0 };
if (Array.isArray(res.svg2ConstructLeft)) {
for (let j = 0; j < res.svg2ConstructLeft.length; j++) {
const seg = res.svg2ConstructLeft[j];
if (!seg || !seg.leftLine) continue;
svg2
.append('path')
.attr('transform', 'translate(0,0)')
.attr('fill', 'none')
.attr('stroke', '#000')
.attr('stroke-width', 2)
.attr('d', line(seg.leftLine));
if (seg.leftLine[1]) {
svg2
.append('path')
.datum(seg.leftLine[1])
.attr('class', 'triangle-point')
.attr('fill', 'rgb(0,0,0)')
.attr('d', this.drawTriangle(triangleLeft))
.attr(
'transform',
(d) => `translate(${x(d.value)},${y(d.depth)})`
);
}
}
}
if (Array.isArray(res.svg2ConstructRight)) {
for (let j = 0; j < res.svg2ConstructRight.length; j++) {
const seg = res.svg2ConstructRight[j];
if (!seg || !seg.rightLine) continue;
svg2
.append('path')
.attr('transform', 'translate(0,0)')
.attr('fill', 'none')
.attr('stroke', '#000')
.attr('stroke-width', 2)
.attr('d', line(seg.rightLine));
if (seg.rightLine[1]) {
svg2
.append('path')
.datum(seg.rightLine[1])
.attr('class', 'triangle-point')
.attr('fill', 'rgb(0,0,0)')
.attr('d', this.drawTriangle(triangleRight))
.attr(
'transform',
(d) => `translate(${x(d.value)},${y(d.depth)})`
);
}
}
}
const arrowLine = d3
.line()
.x((d) => x(d[0]))
.y((d) => y(d[1]))
.curve(d3.curveLinear);
// 左侧标注
if (Array.isArray(res.svg2ConstructPointLeft)) {
for (let i = 0; i < res.svg2ConstructPointLeft.length; i++) {
const item = res.svg2ConstructPointLeft[i];
if (!item || !Array.isArray(item.point) || item.point.length < 2)
continue;
svg2
.append('path')
.attr('d', arrowLine(item.point))
.attr('stroke', '#000')
.attr('fill', 'none')
.attr('stroke-width', 1)
.attr('stroke-linecap', 'round');
const circleX = x(item.point[0][0]);
const circleY = y(item.point[0][1]);
svg2
.append('circle')
.attr('cx', circleX)
.attr('cy', circleY)
.attr('r', 3)
.attr('fill', '#000')
.attr('stroke', 'none');
const textX = x(item.point[0][0] + 20);
const textY = y(item.point[0][1] - 3);
const group = svg2.append('g').attr('transform', 'translate(0,0)');
const text = group
.append('text')
.attr('x', textX + 10)
.attr('y', textY)
.attr('fill', '#000')
.attr('font-size', '12px');
if (item.describe1) {
text
.append('tspan')
.attr('x', textX + 10)
.attr('y', textY)
.attr('font-weight', 'bold')
.text(item.describe1);
}
if (item.describe2) {
text
.append('tspan')
.attr('x', textX + 10)
.attr('y', textY + 16)
.attr('font-weight', 'bold')
.text(item.describe2);
}
if (item.describe3) {
text
.append('tspan')
.attr('x', textX + 10)
.attr('y', textY + 32)
.attr('font-weight', 'bold')
.text(item.describe3);
}
}
}
// 右侧标注
if (Array.isArray(res.svg2ConstructPointRight)) {
for (let i = 0; i < res.svg2ConstructPointRight.length; i++) {
const item = res.svg2ConstructPointRight[i];
if (!item || !Array.isArray(item.point) || item.point.length < 2)
continue;
svg2
.append('path')
.attr('d', arrowLine(item.point))
.attr('stroke', '#000')
.attr('fill', 'none')
.attr('stroke-width', 1)
.attr('stroke-linecap', 'round');
const circleX = x(item.point[0][0]);
const circleY = y(item.point[0][1]);
svg2
.append('circle')
.attr('cx', circleX)
.attr('cy', circleY)
.attr('r', 3)
.attr('fill', '#000')
.attr('stroke', 'none');
const textX = x(item.point[0][0] + 20);
const textY = y(item.point[0][1] - 3);
const group = svg2.append('g').attr('transform', 'translate(0,0)');
const text = group
.append('text')
.attr('x', textX + 10)
.attr('y', textY)
.attr('fill', '#000')
.attr('font-size', '12px');
if (item.describe1) {
text
.append('tspan')
.attr('x', textX + 10)
.attr('y', textY)
.attr('font-weight', 'bold')
.text(item.describe1);
}
if (item.describe2) {
text
.append('tspan')
.attr('x', textX + 10)
.attr('y', textY + 16)
.attr('font-weight', 'bold')
.text(item.describe2);
}
if (item.describe3) {
text
.append('tspan')
.attr('x', textX + 10)
.attr('y', textY + 32)
.attr('font-weight', 'bold')
.text(item.describe3);
}
}
}
},
drawTriangle(triangle) {
return (
'M' +
triangle.x1 +
',' +
triangle.y1 +
'L' +
triangle.x2 +
',' +
triangle.y2 +
'L' +
triangle.x3 +
',' +
triangle.y3 +
'Z'
);
}
}
};
</script>
<style scoped lang="scss">
.multi-well-structure {
padding: 10px 0;
}
.well-col {
margin-bottom: 10px;
}
.jsjgt-wrapper {
height: calc(100vh - 140px);
min-height: 760px;
border: 1px solid #e4e7ed;
border-radius: 4px;
display: flex;
flex-direction: column;
background-color: #fff;
overflow: hidden;
}
.jsjgt-title {
padding: 6px 12px;
font-size: 14px;
font-weight: 600;
color: #333;
border-bottom: 1px solid #e4e7ed;
background-color: #f5f7fa;
}
.jsjgt-body {
flex: 1;
display: flex;
align-items: stretch;
justify-content: center;
position: relative;
padding: 0;
}
.jsjgt-body .svg2 {
flex: 1;
display: block;
width: 100%;
height: 100%;
}
.jsjgt-empty {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: #c0c4cc;
}
</style>
<template>
<div class="multi-well-trajectory" v-loading="isLoading" element-loading-text="加载中...">
<el-row :gutter="10">
<el-col v-for="well in wellList" :key="well" :xs="24" :sm="24" :md="12" :lg="12" :xl="12" class="well-col">
<div class="traj-wrapper">
<div class="traj-title">{{ well }}</div>
<div class="traj-body">
<div class="legend">
<div class="legendItem">
<div class="colorLump" style="background: rgb(219, 67, 83)"></div>
<div class="text">狗腿度(°/30m) &gt;8</div>
</div>
<div class="legendItem">
<div class="colorLump" style="background: rgb(221, 68, 166)"></div>
<div class="text">狗腿度(°/30m) 6-8</div>
</div>
<div class="legendItem">
<div class="colorLump" style="background: rgb(89, 95, 246)"></div>
<div class="text">狗腿度(°/30m) 4-6</div>
</div>
<div class="legendItem">
<div class="colorLump" style="background: rgb(76, 167, 248)"></div>
<div class="text">狗腿度(°/30m) 2-4</div>
</div>
<div class="legendItem">
<div class="colorLump" style="background: rgb(124, 233, 234)"></div>
<div class="text">狗腿度(°/30m) &lt;2</div>
</div>
</div>
<div :ref="chartRefKey(well)" class="chart"></div>
<div v-if="!loadedMap[well]" class="traj-empty">
{{ loadingMap[well] ? '加载中...' : '暂无数据' }}
</div>
</div>
</div>
</el-col>
</el-row>
</div>
</template>
<script>
import 'echarts-gl';
import * as echarts from 'echarts';
import { getThreeViews } from '@/api/optimization/initialization';
export default {
name: 'MultiWellTrajectory',
props: {
// 主井
jh: {
type: String,
default: ''
},
// 邻井(逗号分隔)
jhs: {
type: String,
default: ''
},
// 是否处于当前激活tab(用于懒加载)
active: {
type: Boolean,
default: false
}
},
data() {
return {
loadingMap: {},
loadedMap: {},
charts: {}
};
},
computed: {
wellList() {
const wells = [];
if (this.jh) wells.push(this.jh);
if (this.jhs) {
this.jhs
.split(',')
.map((w) => w && w.trim())
.filter(Boolean)
.forEach((w) => {
if (!wells.includes(w)) wells.push(w);
});
}
return wells;
},
/** 是否有接口尚未请求完成,用于显示遮罩 */
isLoading() {
return this.wellList.length > 0 && this.wellList.some((w) => this.loadingMap[w]);
}
},
watch: {
active: {
handler(val) {
if (val) {
this.reload();
}
},
immediate: true
},
wellList() {
if (this.active) {
this.reload();
}
}
},
mounted() {
window.addEventListener('resize', this.resizeAll);
},
beforeDestroy() {
window.removeEventListener('resize', this.resizeAll);
Object.keys(this.charts || {}).forEach((k) => {
try {
this.charts[k] && this.charts[k].dispose && this.charts[k].dispose();
} catch (e) { }
});
this.charts = {};
},
methods: {
normalizeWell(well) {
return String(well).replace(/[^a-zA-Z0-9_-]/g, '-');
},
chartRefKey(well) {
return `trajChart_${this.normalizeWell(well)}`;
},
getChartEl(well) {
const key = this.chartRefKey(well);
const ref = this.$refs[key];
if (Array.isArray(ref)) return ref[0] || null;
return ref || null;
},
resizeAll() {
Object.keys(this.charts || {}).forEach((k) => {
try {
this.charts[k] && this.charts[k].resize && this.charts[k].resize();
} catch (e) { }
});
},
// 供父组件在tab点击/切回时主动触发
reload() {
if (!this.active) return;
this.wellList.forEach((well) => this.loadForWell(well));
},
loadForWell(well) {
if (!well) return;
this.$set(this.loadingMap, well, true);
this.$set(this.loadedMap, well, false);
getThreeViews({ jh: well })
.then((res) => {
if (res && res.code === 200 && res.swmap) {
this.$nextTick(() => {
this.draw3D(well, res.swmap);
this.$set(this.loadedMap, well, true);
});
}
})
.catch(() => { })
.finally(() => {
this.$set(this.loadingMap, well, false);
});
},
draw3D(well, data) {
const el = this.getChartEl(well);
if (!el) return;
const key = this.normalizeWell(well);
let chart = this.charts[key];
if (!chart) {
chart = echarts.init(el);
this.$set(this.charts, key, chart);
}
const lineData = (data && Array.isArray(data.lineData) && data.lineData) || [];
const xmin = Number(data && data.xmin);
const xmax = Number(data && data.xmax);
const ymin = Number(data && data.ymin);
const ymax = Number(data && data.ymax);
const zmin = Number(data && data.zmin);
const zmax = Number(data && data.zmax);
const processedData = lineData.map((item) => {
if (!item || !Array.isArray(item.value) || item.value.length < 3) return item;
return {
...item,
value: [item.value[0], item.value[1], zmax - (item.value[2] - zmin)]
};
});
const option = {
tooltip: {
show: true,
formatter: function (params) {
const v = (params && params.value) || [];
const z = typeof v[2] === 'number' ? v[2] : Number(v[2]);
const x = v[0];
const y = v[1];
return `
垂深${(zmax - z).toFixed(0)}m<br>
南北位移${y}m<br>
东西位移${x}m<br>
狗腿度${params.data && params.data.params}°/30m
`;
}
},
backgroundColor: '#fff',
xAxis3D: {
type: 'value',
name: 'x-东西(m)',
min: isFinite(xmin) ? xmin : undefined,
max: isFinite(xmax) ? xmax : undefined,
splitNumber: 5
},
yAxis3D: {
type: 'value',
name: 'y-南北(m)',
min: isFinite(ymin) ? ymin : undefined,
max: isFinite(ymax) ? ymax : undefined,
splitNumber: 5
},
zAxis3D: {
type: 'value',
name: '垂深(m)',
position: 'right',
min: isFinite(zmin) ? Number(zmin.toFixed(0)) : undefined,
max: isFinite(zmax) ? Number(zmax.toFixed(0)) : undefined,
axisLabel: {
formatter: function (value) {
return (zmin - value).toFixed(0);
}
},
splitNumber: 5
},
grid3D: {
viewControl: {
alpha: 0
}
},
series: [
{
type: 'line3D',
data: processedData,
lineStyle: {
width: 4,
color: (params) => {
const gtd = params && params.data && params.data.params;
const v = Number(gtd);
return v < 2
? 'rgb(124,233,234)'
: v >= 2 && v < 4
? 'rgb(76,167,248)'
: v >= 4 && v < 6
? 'rgb(89,95,246)'
: v >= 6 && v < 8
? 'rgb(221,68,166)'
: 'rgb(219,67,83)';
}
}
}
]
};
chart.setOption(option, true);
chart.resize();
}
}
};
</script>
<style scoped lang="scss">
.multi-well-trajectory {
padding: 10px 0;
}
.well-col {
margin-bottom: 10px;
}
.traj-wrapper {
height: calc(100vh - 260px);
min-height: 520px;
border: 1px solid #e4e7ed;
border-radius: 4px;
display: flex;
flex-direction: column;
background-color: #fff;
overflow: hidden;
}
.traj-title {
padding: 6px 12px;
font-size: 14px;
font-weight: 600;
color: #333;
border-bottom: 1px solid #e4e7ed;
background-color: #f5f7fa;
}
.traj-body {
flex: 1;
position: relative;
}
.legend {
position: absolute;
top: 12px;
right: 12px;
z-index: 10;
background: rgba(255, 255, 255, 0.85);
border: 1px solid #ebeef5;
border-radius: 6px;
padding: 10px 10px 2px 10px;
}
.legendItem {
display: flex;
align-items: center;
margin-bottom: 8px;
}
.colorLump {
width: 25px;
height: 15px;
line-height: 15px;
border-radius: 5px;
margin-right: 6px;
}
.text {
font-size: 12px;
height: 15px;
line-height: 15px;
white-space: nowrap;
}
.chart {
width: 100%;
height: 100%;
}
.traj-empty {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: #c0c4cc;
z-index: 5;
}
</style>
<template>
<div class="mini-chart-wrapper">
<div
v-if="enablePreview"
class="mini-chart-toolbar"
@click.stop="handlePreview"
>
<i class="el-icon-zoom-in"></i>
</div>
<div class="mini-chart" ref="chartRef"></div>
</div>
</template>
<script>
import * as echarts from "echarts";
export default {
name: "WellDrillingTimeMiniChart",
props: {
jh: { type: String, default: "" },
rows: { type: Array, default: () => [] },
height: { type: [Number, String], default: 180 },
enablePreview: { type: Boolean, default: true },
},
data() {
return {
myChart: null,
resizeObserver: null,
};
},
watch: {
rows: {
handler() {
this.$nextTick(() => this.render());
},
deep: true,
},
height: {
handler() {
this.applyHeight();
this.$nextTick(() => this.resize());
},
},
},
mounted() {
this.applyHeight();
this.init();
this.observeResize();
},
beforeDestroy() {
this.cleanup();
},
methods: {
handlePreview() {
this.$emit("preview", {
jh: this.jh,
rows: this.rows,
});
},
applyHeight() {
const el = this.$refs.chartRef;
if (!el) return;
const h = typeof this.height === "number" ? `${this.height}px` : `${this.height}`;
el.style.height = h;
el.style.width = "100%";
},
init() {
const el = this.$refs.chartRef;
if (!el) return;
if (this.myChart) {
this.myChart.dispose();
}
this.myChart = echarts.init(el, null, { renderer: "canvas", useDirtyRect: true });
this.render();
},
cleanup() {
if (this.resizeObserver) {
this.resizeObserver.disconnect();
this.resizeObserver = null;
}
if (this.myChart) {
this.myChart.dispose();
this.myChart = null;
}
},
observeResize() {
const el = this.$refs.chartRef;
if (!el || !window.ResizeObserver) return;
this.resizeObserver = new ResizeObserver(() => {
this.resize();
});
this.resizeObserver.observe(el);
},
resize() {
if (this.myChart) this.myChart.resize();
},
detectDepthKey(rows) {
const candidates = ["js", "depth", "dept", "jd", "md"];
for (const k of candidates) {
if (rows.some((r) => r && r[k] !== undefined && r[k] !== null)) return k;
}
return null;
},
buildFormationIntervals(rows, depthKey) {
if (!depthKey) return [];
const hasCw = rows.some((r) => r && r.cw != null && r.cw !== "");
if (!hasCw) return [];
const sorted = rows
.filter((r) => r && r[depthKey] != null)
.map((r) => ({ ...r, __d: Number(r[depthKey]) }))
.filter((r) => Number.isFinite(r.__d))
.sort((a, b) => a.__d - b.__d);
if (!sorted.length) return [];
const intervals = [];
let current = String(sorted[0].cw ?? "");
let startDepth = sorted[0].__d;
for (let i = 1; i < sorted.length; i++) {
const cw = String(sorted[i].cw ?? "");
if (cw !== current) {
intervals.push({ formation: current || "-", startDepth, endDepth: sorted[i - 1].__d });
current = cw;
startDepth = sorted[i].__d;
}
}
intervals.push({ formation: current || "-", startDepth, endDepth: sorted[sorted.length - 1].__d });
return intervals;
},
convertSeries(rows, depthKey, valueKey) {
if (!depthKey) return [];
return (rows || [])
.map((r) => {
if (!r) return null;
const d = Number(r[depthKey]);
const v = r[valueKey];
const val = v === "" ? null : Number(v);
if (!Number.isFinite(d) || !Number.isFinite(val)) return null;
return [d, val];
})
.filter(Boolean)
.sort((a, b) => a[0] - b[0]);
},
render() {
if (!this.myChart) return;
const rows = Array.isArray(this.rows) ? this.rows : [];
if (!rows.length) {
this.myChart.clear();
this.myChart.setOption({
graphic: [
{
type: "text",
left: "center",
top: "middle",
style: {
text: "暂无录井曲线数据",
fill: "#9ca3af",
fontSize: 12,
fontWeight: 500,
},
silent: true,
},
],
});
return;
}
const depthKey = this.detectDepthKey(rows);
if (!depthKey) {
this.myChart.clear();
this.myChart.setOption({
graphic: [
{
type: "text",
left: "center",
top: "middle",
style: { text: "缺少深度字段", fill: "#ef4444", fontSize: 12, fontWeight: 600 },
silent: true,
},
],
});
return;
}
const keyMap = [
{ key: "zs", name: "钻时 (min/m)", color: "#FF0000", yAxisIndex: 0, width: 2.5 },
{ key: "nj", name: "扭矩 (kN•m)", color: "#1E90FF", yAxisIndex: 0, width: 1.2 },
{ key: "ly", name: "立压 (MPa)", color: "#32CD32", yAxisIndex: 0, width: 1.2 },
{ key: "zy", name: "钻压 (kN)", color: "#FFD700", yAxisIndex: 1, width: 1.2 },
{ key: "zs1", name: "转速 (rpm)", color: "#FF6347", yAxisIndex: 1, width: 1.2 },
{ key: "zbc", name: "泵冲", color: "#9370DB", yAxisIndex: 1, width: 1.2 },
{ key: "rkll", name: "入口流量", color: "#20B2AA", yAxisIndex: 1, width: 1.2 },
];
const series = [];
const allDepths = [];
for (const m of keyMap) {
const has = rows.some((r) => r && r[m.key] !== undefined && r[m.key] !== null && r[m.key] !== "");
if (!has) continue;
const data = this.convertSeries(rows, depthKey, m.key);
if (!data.length) continue;
allDepths.push(...data.map((d) => d[0]));
series.push({
name: m.name,
type: "line",
data,
yAxisIndex: m.yAxisIndex,
smooth: true,
symbol: "none",
lineStyle: { color: m.color, width: m.width },
});
}
if (!series.length) {
this.myChart.clear();
this.myChart.setOption({
graphic: [
{
type: "text",
left: "center",
top: "middle",
style: { text: "无可用曲线字段", fill: "#9ca3af", fontSize: 12, fontWeight: 500 },
silent: true,
},
],
});
return;
}
const minDepth = Math.min(...allDepths);
const maxDepth = Math.max(...allDepths);
const pad = Math.max(5, (maxDepth - minDepth) * 0.03);
const formationIntervals = this.buildFormationIntervals(rows, depthKey);
const formationMarkAreas = formationIntervals.map((it) => [
{ xAxis: it.startDepth, name: it.formation, itemStyle: { color: "rgba(148,163,184,0.18)" } },
{ xAxis: it.endDepth },
]);
const option = {
animation: false,
grid: { left: 40, right: 16, top: 10, bottom: 26, containLabel: false },
tooltip: {
trigger: "axis",
axisPointer: { type: "cross" },
formatter: (params) => {
if (!params || !params.length) return "";
const depth = params[0].axisValue ?? (params[0].data && params[0].data[0]) ?? "";
let html = `<div style="font-weight:600;margin-bottom:6px;">井深: ${depth}</div>`;
params.forEach((p) => {
const v = Array.isArray(p.value) ? p.value[1] : p.value;
if (v == null) return;
html += `<div style="margin:2px 0;">
<span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:${p.color};margin-right:6px;"></span>
<span style="font-weight:500;">${p.seriesName}:</span>
<span style="margin-left:6px;font-weight:600;">${v}</span>
</div>`;
});
return html;
},
},
xAxis: {
type: "value",
min: minDepth - pad,
max: maxDepth + pad,
axisLabel: { fontSize: 10, color: "#64748b" },
axisLine: { lineStyle: { color: "rgba(148,163,184,0.7)" } },
splitLine: { show: true, lineStyle: { type: "dashed", color: "rgba(148,163,184,0.25)" } },
},
yAxis: [
{
type: "value",
axisLabel: { fontSize: 10, color: "#64748b" },
axisLine: { lineStyle: { color: "rgba(148,163,184,0.7)" } },
splitLine: { show: false },
},
{
type: "value",
axisLabel: { fontSize: 10, color: "#64748b" },
axisLine: { lineStyle: { color: "rgba(148,163,184,0.7)" } },
splitLine: { show: false },
},
],
series: [
...(formationMarkAreas.length
? [
{
name: "formationMarkArea",
type: "line",
data: [],
silent: true,
lineStyle: { width: 0 },
markArea: { silent: true, data: formationMarkAreas },
legendHoverLink: false,
},
]
: []),
...series,
],
};
this.myChart.setOption(option, true);
this.resize();
},
},
};
</script>
<style lang="scss" scoped>
.mini-chart-wrapper {
position: relative;
width: 100%;
height: 100%;
}
.mini-chart-toolbar {
position: absolute;
top: 6px;
right: 8px;
z-index: 2;
width: 22px;
height: 22px;
border-radius: 999px;
background: rgba(15, 23, 42, 0.06);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s ease;
}
.mini-chart-toolbar:hover {
background: rgba(37, 99, 235, 0.18);
transform: translateY(-1px);
}
.mini-chart-toolbar .el-icon-zoom-in {
font-size: 14px;
color: #4b5563;
}
.mini-chart {
width: 100%;
height: 180px;
border-radius: 10px;
background: #ffffff;
border: 1px solid rgba(15, 23, 42, 0.08);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.06);
overflow: hidden;
}
</style>
...@@ -4,17 +4,27 @@ ...@@ -4,17 +4,27 @@
<el-tabs v-model="activeTab" type="card" @tab-click="handleTabClick" style="margin-top: -10px;"> <el-tabs v-model="activeTab" type="card" @tab-click="handleTabClick" style="margin-top: -10px;">
<el-tab-pane label="钻头使用数据" name="dataTable"> <el-tab-pane label="钻头使用数据" name="dataTable">
<DataTable ref="dataTableRef" :jh="queryParams.jh" :famc="queryParams.famc" :qk="queryParams.qk" <DataTable ref="dataTableRef" :jh="queryParams.jh" :famc="queryParams.famc" :qk="queryParams.qk"
:jhs="queryParams.jhs"/> :jhs="queryParams.jhs" />
</el-tab-pane> </el-tab-pane>
<el-tab-pane label="钻头优选" name="curveGraph"> <el-tab-pane label="钻头优选" name="curveGraph">
<CurveGraph ref="curveGraphRef" :jh="queryParams.jh" :famc="queryParams.famc" :qk="queryParams.qk" <CurveGraph ref="curveGraphRef" :jh="queryParams.jh" :famc="queryParams.famc" :qk="queryParams.qk"
:jhs="queryParams.jhs" :id="queryParams.id" :kcjs="queryParams.kc" :ztccsjs="queryParams.ztccs" /> :jhs="queryParams.jhs" :id="queryParams.id" :kcjs="queryParams.kc" :ztccsjs="queryParams.ztccs" />
</el-tab-pane> </el-tab-pane>
<el-tab-pane label="钻头能效分析" name="histogramGraph"> <el-tab-pane label="钻头能效分析" name="histogramGraph">
<HistogramGraph ref="histogramGraphRef" :jh="queryParams.jh" :famc="queryParams.famc" <HistogramGraph ref="histogramGraphRef" :jh="queryParams.jh" :famc="queryParams.famc" :qk="queryParams.qk"
:qk="queryParams.qk" :jhs="queryParams.jhs"/> :jhs="queryParams.jhs" />
</el-tab-pane>
<el-tab-pane label="井身结构" name="wellStructure">
<MultiWellStructure ref="multiWellStructureRef" :active="activeTab === 'wellStructure'" :jh="queryParams.jh"
:jhs="queryParams.jhs" />
</el-tab-pane>
<el-tab-pane label="井眼轨迹" name="wellboreTrajectory">
<MultiWellTrajectory ref="multiWellTrajectoryRef" :active="activeTab === 'wellboreTrajectory'"
:jh="queryParams.jh" :jhs="queryParams.jhs" />
</el-tab-pane> </el-tab-pane>
</el-tabs> </el-tabs>
</div> </div>
...@@ -24,13 +34,17 @@ ...@@ -24,13 +34,17 @@
import DataTable from './components/DataTable.vue'; import DataTable from './components/DataTable.vue';
import CurveGraph from './components/CurveGraph.vue'; import CurveGraph from './components/CurveGraph.vue';
import HistogramGraph from './components/HistogramGraph.vue'; import HistogramGraph from './components/HistogramGraph.vue';
import MultiWellStructure from './components/MultiWellStructure.vue';
import MultiWellTrajectory from './components/MultiWellTrajectory.vue';
export default { export default {
name: "DjxxDetail", name: "DjxxDetail",
components: { components: {
DataTable, DataTable,
CurveGraph, CurveGraph,
HistogramGraph HistogramGraph,
MultiWellStructure,
MultiWellTrajectory
}, },
data() { data() {
return { return {
...@@ -58,14 +72,14 @@ export default { ...@@ -58,14 +72,14 @@ export default {
created() { created() {
// 获取路由参数(使用query) // 获取路由参数(使用query)
console.log('详情页面created,完整路由对象:', this.$route); console.log('详情页面created,完整路由对象:', this.$route);
let {id, jh, famc, qk, jhs, kc, ztccs} = this.$route.query || {}; let { id, jh, famc, qk, jhs, kc, ztccs } = this.$route.query || {};
console.log('路由query参数:', {id, jh, famc, qk, jhs, kc, ztccs}); console.log('路由query参数:', { id, jh, famc, qk, jhs, kc, ztccs });
// 优先使用query,其次使用sessionStorage // 优先使用query,其次使用sessionStorage
if (!jh || jh === 'undefined' || jh === 'null') { if (!jh || jh === 'undefined' || jh === 'null') {
try { try {
const cached = JSON.parse(sessionStorage.getItem('djxxDetailParams') || '{}'); const cached = JSON.parse(sessionStorage.getItem('djxxDetailParams') || '{}');
({id, jh, famc, qk, jhs, kc, ztccs} = {...cached, ...this.$route.query}); ({ id, jh, famc, qk, jhs, kc, ztccs } = { ...cached, ...this.$route.query });
console.log('使用缓存参数:', {id, jh, famc, qk, jhs, kc, ztccs}); console.log('使用缓存参数:', { id, jh, famc, qk, jhs, kc, ztccs });
} catch (e) { } catch (e) {
console.warn('读取缓存失败', e); console.warn('读取缓存失败', e);
} }
...@@ -91,7 +105,7 @@ export default { ...@@ -91,7 +105,7 @@ export default {
}, },
activated() { activated() {
// 组件被 keep-alive 缓存时,返回本页会触发此钩子 // 组件被 keep-alive 缓存时,返回本页会触发此钩子
const {id, jh, famc, qk, jhs,kc,ztccs} = this.$route.query || {}; const { id, jh, famc, qk, jhs, kc, ztccs } = this.$route.query || {};
if (jh && jh !== 'undefined' && jh !== 'null') { if (jh && jh !== 'undefined' && jh !== 'null') {
this.queryParams.id = id || ''; this.queryParams.id = id || '';
this.queryParams.jh = jh || ''; this.queryParams.jh = jh || '';
...@@ -106,7 +120,7 @@ export default { ...@@ -106,7 +120,7 @@ export default {
try { try {
const cached = JSON.parse(sessionStorage.getItem('djxxDetailParams') || '{}'); const cached = JSON.parse(sessionStorage.getItem('djxxDetailParams') || '{}');
if (cached && cached.jh) { if (cached && cached.jh) {
this.queryParams = {...this.queryParams, ...cached}; this.queryParams = { ...this.queryParams, ...cached };
this.$nextTick(() => this.refreshActiveTab()); this.$nextTick(() => this.refreshActiveTab());
} }
} catch (e) { } catch (e) {
...@@ -115,7 +129,7 @@ export default { ...@@ -115,7 +129,7 @@ export default {
}, },
beforeRouteUpdate(to, from, next) { beforeRouteUpdate(to, from, next) {
// 同一路由切换不同 query 时触发 // 同一路由切换不同 query 时触发
const {id, jh, famc, qk, jhs,kc,ztccs} = to.query || {}; const { id, jh, famc, qk, jhs, kc, ztccs } = to.query || {};
if (jh && jh !== 'undefined' && jh !== 'null') { if (jh && jh !== 'undefined' && jh !== 'null') {
this.queryParams.id = id || ''; this.queryParams.id = id || '';
this.queryParams.jh = jh || ''; this.queryParams.jh = jh || '';
...@@ -132,7 +146,7 @@ export default { ...@@ -132,7 +146,7 @@ export default {
watch: { watch: {
// 当从列表页连续点击"查看"跳转到同一路由时,组件会复用,这里监听路由变化并刷新当前激活tab // 当从列表页连续点击"查看"跳转到同一路由时,组件会复用,这里监听路由变化并刷新当前激活tab
'$route.query'(to) { '$route.query'(to) {
const {id, jh, famc, qk, jhs,kc,ztccs} = to || {}; const { id, jh, famc, qk, jhs, kc, ztccs } = to || {};
if (jh && jh !== 'undefined' && jh !== 'null') { if (jh && jh !== 'undefined' && jh !== 'null') {
this.queryParams.id = id || ''; this.queryParams.id = id || '';
this.queryParams.jh = jh || ''; this.queryParams.jh = jh || '';
...@@ -165,6 +179,10 @@ export default { ...@@ -165,6 +179,10 @@ export default {
this.$refs.curveGraphRef && this.$refs.curveGraphRef.loadData && this.$refs.curveGraphRef.loadData(); this.$refs.curveGraphRef && this.$refs.curveGraphRef.loadData && this.$refs.curveGraphRef.loadData();
} else if (this.activeTab === 'histogramGraph') { } else if (this.activeTab === 'histogramGraph') {
this.$refs.histogramGraphRef && this.$refs.histogramGraphRef.loadData && this.$refs.histogramGraphRef.loadData(); this.$refs.histogramGraphRef && this.$refs.histogramGraphRef.loadData && this.$refs.histogramGraphRef.loadData();
} else if (this.activeTab === 'wellStructure') {
this.$refs.multiWellStructureRef && this.$refs.multiWellStructureRef.reload && this.$refs.multiWellStructureRef.reload();
} else if (this.activeTab === 'wellboreTrajectory') {
this.$refs.multiWellTrajectoryRef && this.$refs.multiWellTrajectoryRef.reload && this.$refs.multiWellTrajectoryRef.reload();
} }
}, },
/** 搜索按钮操作 */ /** 搜索按钮操作 */
...@@ -207,6 +225,12 @@ export default { ...@@ -207,6 +225,12 @@ export default {
this.$refs.curveGraphRef && this.$refs.curveGraphRef.loadData && this.$refs.curveGraphRef.loadData(); this.$refs.curveGraphRef && this.$refs.curveGraphRef.loadData && this.$refs.curveGraphRef.loadData();
} else if (tab.name === 'histogramGraph') { } else if (tab.name === 'histogramGraph') {
this.$refs.histogramGraphRef && this.$refs.histogramGraphRef.loadData && this.$refs.histogramGraphRef.loadData(); this.$refs.histogramGraphRef && this.$refs.histogramGraphRef.loadData && this.$refs.histogramGraphRef.loadData();
} else if (tab.name === 'wellStructure') {
// 懒加载:仅当点到井身结构tab时才触发接口调用
this.$refs.multiWellStructureRef && this.$refs.multiWellStructureRef.reload && this.$refs.multiWellStructureRef.reload();
} else if (tab.name === 'wellboreTrajectory') {
// 懒加载:仅当点到井眼轨迹tab时才触发接口调用
this.$refs.multiWellTrajectoryRef && this.$refs.multiWellTrajectoryRef.reload && this.$refs.multiWellTrajectoryRef.reload();
} }
} }
} }
...@@ -242,7 +266,7 @@ export default { ...@@ -242,7 +266,7 @@ export default {
} }
} }
::v-deep .el-table__cell > .cell { ::v-deep .el-table__cell>.cell {
font-weight: normal; font-weight: normal;
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment