Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
P
pdczt_qd
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
zhaopanyu
pdczt_qd
Commits
6acd0460
Commit
6acd0460
authored
Mar 06, 2026
by
zhaopanyu
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
zpy
parent
344e397f
Hide whitespace changes
Inline
Side-by-side
Showing
10 changed files
with
9437 additions
and
85 deletions
+9437
-85
src/api/system/jsdb.js
+9
-0
src/views/efficiencyAnalysis/djxx/detail/components/HistogramGraph copy 3.vue
+1992
-0
src/views/efficiencyAnalysis/djxx/detail/components/HistogramGraph copy 4.vue
+1959
-0
src/views/efficiencyAnalysis/djxx/detail/components/HistogramGraph copy 5.vue
+2068
-0
src/views/efficiencyAnalysis/djxx/detail/components/HistogramGraph copy 6.vue
+1822
-0
src/views/efficiencyAnalysis/djxx/detail/components/HistogramGraph.vue
+312
-71
src/views/efficiencyAnalysis/djxx/detail/components/MultiWellStructure.vue
+535
-0
src/views/efficiencyAnalysis/djxx/detail/components/MultiWellTrajectory.vue
+351
-0
src/views/efficiencyAnalysis/djxx/detail/components/WellDrillingTimeMiniChart.vue
+351
-0
src/views/efficiencyAnalysis/djxx/detail/index.vue
+38
-14
No files found.
src/api/system/jsdb.js
View file @
6acd0460
...
@@ -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
})
}
src/views/efficiencyAnalysis/djxx/detail/components/HistogramGraph copy 3.vue
0 → 100644
View file @
6acd0460
<
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
>
src/views/efficiencyAnalysis/djxx/detail/components/HistogramGraph copy 4.vue
0 → 100644
View file @
6acd0460
<
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
>
src/views/efficiencyAnalysis/djxx/detail/components/HistogramGraph copy 5.vue
0 → 100644
View file @
6acd0460
<
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
>
src/views/efficiencyAnalysis/djxx/detail/components/HistogramGraph copy 6.vue
0 → 100644
View file @
6acd0460
<
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
>
src/views/efficiencyAnalysis/djxx/detail/components/HistogramGraph.vue
View file @
6acd0460
<
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
;
...
...
src/views/efficiencyAnalysis/djxx/detail/components/MultiWellStructure.vue
0 → 100644
View file @
6acd0460
<
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
>
src/views/efficiencyAnalysis/djxx/detail/components/MultiWellTrajectory.vue
0 → 100644
View file @
6acd0460
<
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)
>
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)
<
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
>
src/views/efficiencyAnalysis/djxx/detail/components/WellDrillingTimeMiniChart.vue
0 → 100644
View file @
6acd0460
<
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
>
src/views/efficiencyAnalysis/djxx/detail/index.vue
View file @
6acd0460
...
@@ -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
;
}
}
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment