Commit b52c00b8 by zhaopanyu

zpy

parent a930ca57
<template>
<div class="chart-container">
<div id="mainzftdj" class="chart" ref="chartRef"></div>
<div class="chart-wrapper">
<div id="mainzftdj" class="chart" ref="chartRef"></div>
<div v-if="legendItems.length" class="legend-panel">
<div class="legend-title">层位图例</div>
<div class="legend-scroll">
<div
v-for="(item, index) in legendItems"
:key="getLegendKey(item, index)"
class="legend-item"
>
<span class="legend-swatch">
<img
v-if="hasLegendSvg(item)"
:src="getLegendSvgSrc(item)"
alt=""
class="legend-swatch-img"
/>
<span
v-else
class="legend-swatch-color"
:style="{ backgroundColor: getLegendColor(index) }"
></span>
</span>
<span class="legend-label">{{ formatLegendName(item) }}</span>
</div>
</div>
</div>
</div>
<div v-if="loading" class="loading-overlay">
<div class="loading-spinner"></div>
<span>加载中...</span>
......@@ -45,6 +72,7 @@ export default {
lastXAxisLabels: null,
lastDepthIntervals: null,
currentGraphicElements: [],
tlList: [],
};
},
computed: {
......@@ -89,6 +117,9 @@ export default {
}
};
return schemes[this.theme];
},
legendItems() {
return Array.isArray(this.tlList) ? this.tlList : [];
}
},
watch: {
......@@ -231,6 +262,7 @@ export default {
jhs: `${this.jh},${this.jhs}`
});
this.mockData = res?.mockData || {};
this.tlList = res?.tlList || res?.mockData?.tlList || [];
return this.mockData;
} catch (error) {
console.error("获取数据失败:", error);
......@@ -883,215 +915,11 @@ export default {
});
},
// 使用像素坐标绘制地层标签(每口井的每个地层都标注名称)
// 清空地层标签(暂不在图内展示层位名称)
drawStratumLabels(stackedAreas, chartConfig, xAxisLabels) {
if (!this.myChart || !stackedAreas) return;
// 清空当前 graphic
if (!this.myChart) return;
this.currentGraphicElements = [];
this.myChart.setOption({ graphic: { elements: [] } });
const graphics = [];
const depthIntervals = this.mockData?.wellData?.depthIntervals || [];
// 如果为老格式(全局数组),保持原有从右侧标注的策略
if (Array.isArray(stackedAreas)) {
if (stackedAreas.length === 0) return;
let currentDepth = chartConfig.yAxis.min;
const lastXIndex = Math.max(0, (xAxisLabels?.length || 1) - 1);
const xOffset = 16;
stackedAreas.forEach((area) => {
if (!area || !area.points || area.points.length === 0) return;
let thickness;
let centerDepth;
if (area.sjdjsd != null && area.sjhd != null) {
// 业务规则:当 sjdjsd 等于 sjhd 时,厚度即为 sjdjsd 本身,从 0 到 sjdjsd
if (Number(area.sjdjsd) === Number(area.sjhd)) {
thickness = Number(area.sjdjsd);
centerDepth = thickness / 2;
} else {
thickness = Math.abs(Number(area.sjdjsd) - Number(area.sjhd));
centerDepth = (Number(area.sjdjsd) + Number(area.sjhd)) / 2;
}
} else {
const p0 = area.points[0];
const y = Number(p0?.y);
const y2 = p0?.y2 != null ? Number(p0.y2) : null;
if (y2 != null && y2 === y) {
// y2 即为厚度,从 0 到 y2
thickness = y2;
centerDepth = thickness / 2;
} else if (y2 != null && !Number.isNaN(y)) {
thickness = Math.abs(y2 - y);
centerDepth = (y2 + y) / 2;
} else if (!Number.isNaN(y)) {
thickness = y;
centerDepth = thickness / 2;
} else {
thickness = 0;
centerDepth = chartConfig?.yAxis?.min || 0;
}
}
const pixelRight = this.myChart.convertToPixel({ xAxisIndex: 0, yAxisIndex: 0 }, [lastXIndex, centerDepth]);
if (!pixelRight || !Array.isArray(pixelRight) || pixelRight.length < 2) return;
const [pxRight, py] = pixelRight;
graphics.push({
type: 'text',
position: [pxRight + xOffset, py],
style: {
text: area.name,
fontSize: 12,
fontWeight: 600,
fill: '#333',
stroke: '#fff',
lineWidth: 2,
backgroundColor: 'transparent',
align: 'left',
verticalAlign: 'middle'
},
z: 10,
zlevel: 30,
bounding: 'raw',
silent: true
});
currentDepth += thickness;
});
this.currentGraphicElements = graphics;
this.myChart.setOption({ graphic: { elements: graphics } });
return;
}
// 新格式:按井号分组对象。先按 depthIntervals 顺序识别每口井对应的 x 轴段
const segments = [];
let currentJh = null;
let currentMin = Infinity;
let currentMax = -Infinity;
let started = false;
for (let i = 0; i < depthIntervals.length; i++) {
const { jh } = depthIntervals[i] || {};
const idx = i; // 使用 depthIntervals 顺序索引作为 x 轴位置
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 lastIndexByJh = new Map();
(depthIntervals || []).forEach((di, idx) => {
if (di && di.jh != null) {
lastIndexByJh.set(di.jh, idx);
}
});
// 对每个井段,遍历该井的每个地层,按其 points 的实际 x 位置放置标签
// 将相同 x 标签的所有全局索引都存储起来,便于按井段范围内精确匹配
const xIndexMap = new Map(); // key: x 标签, value: 索引数组
(xAxisLabels || []).forEach((x, idx) => {
const key = String(x);
if (!xIndexMap.has(key)) xIndexMap.set(key, []);
xIndexMap.get(key).push(idx);
});
segments.forEach(seg => {
const layers = stackedAreas[seg.jh] || [];
if (!Array.isArray(layers) || layers.length === 0) return;
layers.forEach(area => {
if (!area) return;
// 选取该层的第一个点(通常为该井的首个测点,如 HT2465)用于横向定位
let pointForX = null;
if (Array.isArray(area.points) && area.points.length > 0) {
// 优先选择在本井段 [startIdx, endIdx] 范围内能匹配到的 x
pointForX = area.points.find(p => {
const arr = xIndexMap.get(String(p?.x));
if (!arr || arr.length === 0) return false;
return arr.some(ix => ix >= seg.startIdx && ix <= seg.endIdx);
}) || area.points[0];
}
// 计算中心深度(厚度规则:当 y2==y 或 sjdjsd==sjhd 时,厚度= y2,从 0 到 y2)
let centerDepth;
if (area.sjdjsd != null && area.sjhd != null) {
if (Number(area.sjdjsd) === Number(area.sjhd)) {
centerDepth = Number(area.sjdjsd) / 2;
} else {
centerDepth = (Number(area.sjdjsd) + Number(area.sjhd)) / 2;
}
} else if (pointForX) {
const py1 = Number(pointForX.y);
const py2 = pointForX.y2 != null ? Number(pointForX.y2) : null;
if (py2 != null && py2 === py1) {
centerDepth = py2 / 2;
} else if (py2 != null) {
centerDepth = (py1 + py2) / 2;
} else if (!Number.isNaN(py1)) {
centerDepth = py1 / 2;
} else {
centerDepth = chartConfig?.yAxis?.min || 0;
}
} else {
centerDepth = chartConfig?.yAxis?.min || 0;
}
// 计算横向像素:若能找到该点的 x 在 xAxis 中的索引,就用该索引;否则退回到该井段的中点
let xIndex = null;
if (pointForX && xIndexMap.has(String(pointForX.x))) {
const candidates = xIndexMap.get(String(pointForX.x)) || [];
// 找到位于当前井段范围内的索引;如没有则回退到第一个
const inSeg = candidates.find(ix => ix >= seg.startIdx && ix <= seg.endIdx);
xIndex = inSeg != null ? inSeg : candidates[0];
} else {
xIndex = (seg.startIdx + seg.endIdx) / 2;
}
// 仅在该井最后一列显示地层名
const lastIdxForJh = lastIndexByJh.get(seg.jh);
if (!Number.isInteger(xIndex) || lastIdxForJh === undefined || lastIdxForJh !== xIndex) {
return;
}
const pixel = this.myChart.convertToPixel({ xAxisIndex: 0, yAxisIndex: 0 }, [xIndex, centerDepth]);
if (!pixel || !Array.isArray(pixel) || pixel.length < 2) return;
const [px, py] = pixel;
graphics.push({
type: 'text',
position: [px + 120, py],
style: {
text: area.name || '',
fontSize: 12,
fontWeight: 600,
fill: '#111827',
stroke: 'rgba(255,255,255,0.9)',
lineWidth: 3,
backgroundColor: 'transparent',
align: 'left',
verticalAlign: 'middle'
},
z: 10,
zlevel: 30,
bounding: 'raw',
silent: true
});
});
});
this.currentGraphicElements = graphics;
this.myChart.setOption({ graphic: { elements: graphics } });
},
// 根据 depthIntervals 的顺序按 jh 分段,绘制虚线与顶部井号
......@@ -1497,6 +1325,40 @@ export default {
const merged = kept.concat(graphics);
this.currentGraphicElements = merged;
this.myChart.setOption({ graphic: { elements: merged } });
},
svgToDataUrl(svgString) {
if (!svgString) return null;
try {
const encoded = encodeURIComponent(svgString)
.replace(/'/g, "%27")
.replace(/"/g, "%22");
return `data:image/svg+xml,${encoded}`;
} catch (error) {
console.warn("SVG 转 dataUrl 失败:", error);
return null;
}
},
getLegendColor(index) {
const palette = [
"#6B7280", "#9CA3AF", "#F59E0B", "#F97316", "#EF4444",
"#10B981", "#14B8A6", "#3B82F6", "#6366F1", "#8B5CF6",
"#EC4899", "#0EA5E9", "#84CC16", "#D97706", "#A16207"
];
return palette[index % palette.length];
},
hasLegendSvg(item) {
return Boolean(item && typeof item.svg === "string" && item.svg.trim().length > 0);
},
getLegendSvgSrc(item) {
if (!this.hasLegendSvg(item)) return "";
return this.svgToDataUrl(item.svg);
},
formatLegendName(item) {
return item && item.name ? item.name : '-';
},
getLegendKey(item, index) {
return `${this.formatLegendName(item)}-${index}`;
}
},
};
......@@ -1515,6 +1377,81 @@ export default {
position: relative;
}
.chart-wrapper {
flex: 1;
display: flex;
flex-direction: row;
align-items: stretch;
gap: 16px;
position: relative;
}
.legend-panel {
width: 220px;
min-width: 200px;
max-height: 100%;
padding: 16px;
border-radius: 16px;
background: linear-gradient(135deg, #ffffff 0%, #f8fafc 100%);
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.08);
border: 1px solid rgba(226, 232, 240, 0.8);
display: flex;
flex-direction: column;
gap: 12px;
}
.legend-title {
font-size: 15px;
font-weight: 600;
color: #111827;
letter-spacing: 0.5px;
}
.legend-scroll {
flex: 1;
overflow-y: auto;
padding-right: 4px;
}
.legend-item {
display: flex;
align-items: center;
gap: 10px;
padding: 6px 4px;
}
.legend-swatch {
width: 32px;
height: 18px;
border-radius: 4px;
border: 1px solid rgba(148, 163, 184, 0.6);
flex-shrink: 0;
overflow: hidden;
display: inline-flex;
align-items: center;
justify-content: center;
background: #fff;
}
.legend-swatch-img {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 2px;
}
.legend-swatch-color {
width: 100%;
height: 100%;
border-radius: 2px;
}
.legend-label {
font-size: 13px;
font-weight: 500;
color: #374151;
}
/* 井号显示样式 */
.well-number-display {
position: absolute;
......@@ -1612,10 +1549,22 @@ export default {
padding: 0 10px;
}
.chart-wrapper {
flex-direction: column;
}
.chart {
min-height: 300px;
border-radius: 12px;
}
.legend-panel {
width: 100%;
min-width: auto;
flex-direction: row;
flex-wrap: wrap;
max-height: 220px;
}
}
/* 深色模式支持 */
......
......@@ -88,6 +88,18 @@
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="开次" prop="kc">
<el-input v-model="formData.kc" placeholder="请输入开次" />
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="井眼尺寸" prop="jycc">
<el-input v-model="formData.jycc" placeholder="请输入井眼尺寸" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="选择邻井" prop="jhs">
<div style="cursor: pointer;" @click="handleSelectAdjacentWell">
<el-input v-model="formData.jhs" placeholder="请点击选择邻井" readonly
......@@ -197,6 +209,8 @@ export default {
famc: '',
qk: '',
jhs: '',
kc: '',
jycc: '',
jl: '10000', // 距离默认值10000
wjsjks: null, // 完井时间开始默认五年前的今天
wjsjjs: null, // 完井时间结束默认今天
......@@ -310,6 +324,8 @@ export default {
famc: '',
qk: '',
jhs: '',
kc: '',
jycc: '',
jl: '10000', // 距离默认值10000
wjsjks: this.getFiveYearsAgoDate(), // 完井时间开始默认五年前的今天
wjsjjs: this.getCurrentDate(), // 完井时间结束默认今天
......@@ -445,6 +461,8 @@ export default {
famc: '',
qk: '',
jhs: '',
kc: '',
jycc: '',
jl: '10000', // 距离默认值10000
wjsjks: this.getFiveYearsAgoDate(), // 完井时间开始默认五年前的今天
wjsjjs: this.getCurrentDate(), // 完井时间结束默认今天
......
......@@ -442,7 +442,7 @@ export default {
svg2.append("rect")
.attr("width", this.width)
.attr("height", this.height)
.style("fill", "rgb(38,42,50)");
.style("fill", "transparent"); // 设置背景色为浅灰色
svg2.append("g")
.attr("transform", `translate(0,${this.marginTop})`)
......@@ -491,6 +491,20 @@ export default {
.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];
......@@ -552,6 +566,127 @@ export default {
}
}
}
// 标注用的直线生成器(左右说明箭头)
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", "#ffffff")
.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", "#ffffff")
.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", "#ffffff")
.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", "#ffffff")
.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", "#ffffff")
.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", "#ffffff")
.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 +
......@@ -678,6 +813,47 @@ export default {
margin: 5px;
}
.content-row {
height: 100%;
}
.jsjgt-wrapper {
height: calc(100vh - 260px);
border: 1px solid #e4e7ed;
border-radius: 4px;
display: flex;
flex-direction: column;
background-color: #262a32;
overflow: hidden;
}
.jsjgt-title {
padding: 6px 12px;
font-size: 14px;
font-weight: 600;
color: #ffffff;
border-bottom: 1px solid #3c3f45;
}
.jsjgt-body {
flex: 1;
display: flex;
align-items: stretch;
justify-content: center;
}
.jsjgt-body .svg2 {
flex: 1;
}
.jsjgt-empty {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: #c0c4cc;
}
::v-deep .el-table__cell>.cell {
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