|
@@ -0,0 +1,731 @@
|
|
|
|
|
+<script lang="ts" setup>
|
|
|
|
|
+import { DigitalApi, type DigitalWell, type WellLogDataset } from '@/api/digital'
|
|
|
|
|
+import { Search } from '@element-plus/icons-vue'
|
|
|
|
|
+import * as echarts from 'echarts'
|
|
|
|
|
+
|
|
|
|
|
+defineOptions({ name: 'DigitalWellLog' })
|
|
|
|
|
+
|
|
|
|
|
+type CurveKey =
|
|
|
|
|
+ | 'sws'
|
|
|
|
|
+ | 'dts'
|
|
|
|
|
+ | 'porns'
|
|
|
|
|
+ | 'rhobs'
|
|
|
|
|
+ | 'rxos'
|
|
|
|
|
+ | 'r25s'
|
|
|
|
|
+ | 'cilds'
|
|
|
|
|
+ | 'calis'
|
|
|
|
|
+ | 'sps'
|
|
|
|
|
+ | 'grs'
|
|
|
|
|
+
|
|
|
|
|
+interface CurveConfig {
|
|
|
|
|
+ name: string
|
|
|
|
|
+ key: CurveKey
|
|
|
|
|
+ unit: string
|
|
|
|
|
+ min: number
|
|
|
|
|
+ max: number
|
|
|
|
|
+ color: string
|
|
|
|
|
+ track: number
|
|
|
|
|
+ inverse?: boolean
|
|
|
|
|
+ lineType?: 'solid' | 'dashed' | 'dotted'
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const curves: CurveConfig[] = [
|
|
|
|
|
+ { name: 'GR', key: 'grs', unit: 'API', min: 30, max: 300, color: '#16a34a', track: 2 },
|
|
|
|
|
+ { name: 'SP', key: 'sps', unit: 'mV', min: 10, max: 300, color: '#2563eb', track: 2 },
|
|
|
|
|
+ {
|
|
|
|
|
+ name: 'CALI',
|
|
|
|
|
+ key: 'calis',
|
|
|
|
|
+ unit: 'mm',
|
|
|
|
|
+ min: 450,
|
|
|
|
|
+ max: 800,
|
|
|
|
|
+ color: '#dc2626',
|
|
|
|
|
+ track: 3,
|
|
|
|
|
+ lineType: 'dashed'
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ name: 'RESX',
|
|
|
|
|
+ key: 'rxos',
|
|
|
|
|
+ unit: 'ohm.m',
|
|
|
|
|
+ min: 1,
|
|
|
|
|
+ max: 30,
|
|
|
|
|
+ color: '#f97316',
|
|
|
|
|
+ track: 0,
|
|
|
|
|
+ lineType: 'dotted'
|
|
|
|
|
+ },
|
|
|
|
|
+ { name: 'RESS', key: 'r25s', unit: 'ohm.m', min: 1, max: 30, color: '#d946ef', track: 0 },
|
|
|
|
|
+ { name: 'RESD', key: 'cilds', unit: 'ohm·m', min: 1, max: 900, color: '#2563eb', track: 3 },
|
|
|
|
|
+ { name: 'RHOB', key: 'rhobs', unit: 'g/cm³', min: 1.95, max: 2.95, color: '#111827', track: 0 },
|
|
|
|
|
+ {
|
|
|
|
|
+ name: 'NPHI',
|
|
|
|
|
+ key: 'porns',
|
|
|
|
|
+ unit: 'v/v',
|
|
|
|
|
+ min: 0,
|
|
|
|
|
+ max: 1,
|
|
|
|
|
+ color: '#2563eb',
|
|
|
|
|
+ track: 0
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ name: 'DTC_SINT',
|
|
|
|
|
+ key: 'dts',
|
|
|
|
|
+ unit: 'us/m',
|
|
|
|
|
+ min: 30,
|
|
|
|
|
+ max: 200,
|
|
|
|
|
+ color: '#7c3aed',
|
|
|
|
|
+ track: 1,
|
|
|
|
|
+ lineType: 'dashed'
|
|
|
|
|
+ },
|
|
|
|
|
+ { name: 'SW', key: 'sws', unit: 'v/v_decimal', min: 20, max: 100, color: '#0891b2', track: 1 }
|
|
|
|
|
+]
|
|
|
|
|
+
|
|
|
|
|
+const trackNames = [
|
|
|
|
|
+ '低量程(约 0~30)',
|
|
|
|
|
+ '中低量程(约 20~200)',
|
|
|
|
|
+ '中量程(约 10~300)',
|
|
|
|
|
+ '高量程(约 450~900)'
|
|
|
|
|
+]
|
|
|
|
|
+const trackCurves = trackNames.map((_, track) => curves.filter((curve) => curve.track === track))
|
|
|
|
|
+const trackLayout = [
|
|
|
|
|
+ { left: '7%', width: '21%' },
|
|
|
|
|
+ { left: '29%', width: '21%' },
|
|
|
|
|
+ { left: '51%', width: '21%' },
|
|
|
|
|
+ { left: '73%', width: '21%' }
|
|
|
|
|
+]
|
|
|
|
|
+const SAMPLE_LIMIT = 2000
|
|
|
|
|
+
|
|
|
|
|
+const message = useMessage()
|
|
|
|
|
+const chartRef = ref<HTMLElement>()
|
|
|
|
|
+const wells = ref<DigitalWell[]>([])
|
|
|
|
|
+const selectedWellId = ref<number>()
|
|
|
|
|
+const keyword = ref('')
|
|
|
|
|
+const dataset = shallowRef<WellLogDataset>()
|
|
|
|
|
+const listLoading = ref(false)
|
|
|
|
|
+const dataLoading = ref(false)
|
|
|
|
|
+const sampledIndices = shallowRef<number[]>([])
|
|
|
|
|
+const chart = shallowRef<echarts.ECharts>()
|
|
|
|
|
+let resizeObserver: ResizeObserver | undefined
|
|
|
|
|
+
|
|
|
|
|
+const filteredWells = computed(() => {
|
|
|
|
|
+ const word = keyword.value.trim().toLowerCase()
|
|
|
|
|
+ if (!word) return wells.value
|
|
|
|
|
+ return wells.value.filter((well) =>
|
|
|
|
|
+ `${well.well ?? ''} ${well.uwi ?? ''}`.toLowerCase().includes(word)
|
|
|
|
|
+ )
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+const selectedWell = computed(() => {
|
|
|
|
|
+ return wells.value.find((well) => well.id === selectedWellId.value) ?? dataset.value?.wellInfo
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+const originalCount = computed(() => dataset.value?.depths?.length ?? 0)
|
|
|
|
|
+const depthRange = computed(() => {
|
|
|
|
|
+ const depths = dataset.value?.depths ?? []
|
|
|
|
|
+ let min = Number.POSITIVE_INFINITY
|
|
|
|
|
+ let max = Number.NEGATIVE_INFINITY
|
|
|
|
|
+ for (const rawDepth of depths) {
|
|
|
|
|
+ const depth = Number(rawDepth)
|
|
|
|
|
+ if (!Number.isFinite(depth)) continue
|
|
|
|
|
+ if (depth < min) min = depth
|
|
|
|
|
+ if (depth > max) max = depth
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!Number.isFinite(min) || !Number.isFinite(max)) return '--'
|
|
|
|
|
+ return `${min.toFixed(2)} – ${max.toFixed(2)} m`
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+function getWellName(well?: DigitalWell) {
|
|
|
|
|
+ return well?.well || well?.uwi || (well?.id ? `井 ${well.id}` : '--')
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 按深度等距寻找最近点,只生成一次索引;所有曲线严格复用同一组索引。
|
|
|
|
|
+ * 这样既适配不等间距深度,也不会发生曲线和深度错位。
|
|
|
|
|
+ */
|
|
|
|
|
+function createDepthSampleIndices(depths: number[], limit: number) {
|
|
|
|
|
+ const valid = depths
|
|
|
|
|
+ .map((depth, index) => ({ depth: Number(depth), index }))
|
|
|
|
|
+ .filter((item) => Number.isFinite(item.depth))
|
|
|
|
|
+ if (valid.length <= limit) return valid.map((item) => item.index)
|
|
|
|
|
+
|
|
|
|
|
+ const first = valid[0]
|
|
|
|
|
+ const last = valid[valid.length - 1]
|
|
|
|
|
+ const ascending = last.depth >= first.depth
|
|
|
|
|
+ const result = new Set<number>([first.index, last.index])
|
|
|
|
|
+ let cursor = 0
|
|
|
|
|
+
|
|
|
|
|
+ for (let point = 1; point < limit - 1; point += 1) {
|
|
|
|
|
+ const target = first.depth + ((last.depth - first.depth) * point) / (limit - 1)
|
|
|
|
|
+ while (cursor < valid.length - 2) {
|
|
|
|
|
+ const currentDistance = Math.abs(valid[cursor].depth - target)
|
|
|
|
|
+ const nextDistance = Math.abs(valid[cursor + 1].depth - target)
|
|
|
|
|
+ const passedTarget = ascending
|
|
|
|
|
+ ? valid[cursor + 1].depth > target
|
|
|
|
|
+ : valid[cursor + 1].depth < target
|
|
|
|
|
+ if (nextDistance > currentDistance && passedTarget) break
|
|
|
|
|
+ cursor += 1
|
|
|
|
|
+ }
|
|
|
|
|
+ result.add(valid[cursor].index)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return Array.from(result).sort((a, b) => a - b)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function curveValue(curve: CurveConfig, index: number): number | null {
|
|
|
|
|
+ const raw = (dataset.value?.[curve.key] as number[] | undefined)?.[index]
|
|
|
|
|
+ const value = Number(raw)
|
|
|
|
|
+ if (!Number.isFinite(value) || value <= -900) return null
|
|
|
|
|
+ // SW 接口为 0~1 小数时换算成用户给定的 20~100 显示范围。
|
|
|
|
|
+ if (curve.key === 'sws' && Math.abs(value) <= 1.5) return value * 100
|
|
|
|
|
+ return value
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function formatNumber(value: unknown) {
|
|
|
|
|
+ const number = Number(value)
|
|
|
|
|
+ if (!Number.isFinite(number)) return '--'
|
|
|
|
|
+ return Math.abs(number) >= 100
|
|
|
|
|
+ ? number.toFixed(1)
|
|
|
|
|
+ : number.toFixed(3).replace(/0+$/, '').replace(/\.$/, '')
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function buildChartOption(): echarts.EChartsOption {
|
|
|
|
|
+ const depths = dataset.value?.depths ?? []
|
|
|
|
|
+ const indices = sampledIndices.value
|
|
|
|
|
+ const sampledDepths = indices.map((index) => Number(depths[index])).filter(Number.isFinite)
|
|
|
|
|
+ const minDepth = sampledDepths.length ? Math.min(...sampledDepths) : 0
|
|
|
|
|
+ const maxDepth = sampledDepths.length ? Math.max(...sampledDepths) : 1
|
|
|
|
|
+
|
|
|
|
|
+ const xAxis: any[] = curves.map((curve) => ({
|
|
|
|
|
+ type: 'value',
|
|
|
|
|
+ gridIndex: curve.track,
|
|
|
|
|
+ min: curve.min,
|
|
|
|
|
+ max: curve.max,
|
|
|
|
|
+ inverse: curve.inverse,
|
|
|
|
|
+ show: false,
|
|
|
|
|
+ splitNumber: 5
|
|
|
|
|
+ }))
|
|
|
|
|
+
|
|
|
|
|
+ const yAxis: any[] = trackLayout.map((_, track) => ({
|
|
|
|
|
+ type: 'value' as const,
|
|
|
|
|
+ gridIndex: track,
|
|
|
|
|
+ min: minDepth,
|
|
|
|
|
+ max: maxDepth,
|
|
|
|
|
+ inverse: true,
|
|
|
|
|
+ axisLine: { show: true, lineStyle: { color: '#64748b', width: 1 } },
|
|
|
|
|
+ axisTick: { show: track === 0, length: 5, lineStyle: { color: '#64748b' } },
|
|
|
|
|
+ axisLabel: {
|
|
|
|
|
+ show: track === 0,
|
|
|
|
|
+ margin: 9,
|
|
|
|
|
+ color: '#334155',
|
|
|
|
|
+ fontSize: 11,
|
|
|
|
|
+ formatter: (value: number) => value.toFixed(1)
|
|
|
|
|
+ },
|
|
|
|
|
+ splitLine: { show: true, lineStyle: { color: '#cbd5e1', width: 1 } },
|
|
|
|
|
+ minorTick: { show: true, splitNumber: 5 },
|
|
|
|
|
+ minorSplitLine: { show: true, lineStyle: { color: '#eef2f7' } },
|
|
|
|
|
+ axisPointer: {
|
|
|
|
|
+ show: true,
|
|
|
|
|
+ snap: true,
|
|
|
|
|
+ label: {
|
|
|
|
|
+ show: track === 0,
|
|
|
|
|
+ formatter: ({ value }: { value: number }) => `${Number(value).toFixed(2)} m`
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }))
|
|
|
|
|
+
|
|
|
|
|
+ const series = curves.map((curve, curveIndex) => ({
|
|
|
|
|
+ name: curve.name,
|
|
|
|
|
+ type: 'line' as const,
|
|
|
|
|
+ xAxisIndex: curveIndex,
|
|
|
|
|
+ yAxisIndex: curve.track,
|
|
|
|
|
+ data: indices.map((index) => [curveValue(curve, index), Number(depths[index])]),
|
|
|
|
|
+ showSymbol: false,
|
|
|
|
|
+ symbol: 'none',
|
|
|
|
|
+ connectNulls: false,
|
|
|
|
|
+ animation: false,
|
|
|
|
|
+ clip: true,
|
|
|
|
|
+ progressive: 4000,
|
|
|
|
|
+ progressiveThreshold: 8000,
|
|
|
|
|
+ lineStyle: { color: curve.color, width: 1.25, type: curve.lineType ?? 'solid' },
|
|
|
|
|
+ emphasis: { disabled: true }
|
|
|
|
|
+ }))
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ animation: false,
|
|
|
|
|
+ backgroundColor: '#ffffff',
|
|
|
|
|
+ grid: trackLayout.map((layout) => ({
|
|
|
|
|
+ ...layout,
|
|
|
|
|
+ top: 8,
|
|
|
|
|
+ bottom: 32,
|
|
|
|
|
+ containLabel: false,
|
|
|
|
|
+ show: true,
|
|
|
|
|
+ borderColor: '#64748b',
|
|
|
|
|
+ borderWidth: 1
|
|
|
|
|
+ })),
|
|
|
|
|
+ xAxis,
|
|
|
|
|
+ yAxis,
|
|
|
|
|
+ series,
|
|
|
|
|
+ axisPointer: {
|
|
|
|
|
+ link: [{ yAxisIndex: [0, 1, 2, 3] }],
|
|
|
|
|
+ lineStyle: { color: '#ef4444', type: 'dashed' }
|
|
|
|
|
+ },
|
|
|
|
|
+ tooltip: {
|
|
|
|
|
+ trigger: 'axis',
|
|
|
|
|
+ confine: true,
|
|
|
|
|
+ backgroundColor: 'rgba(15, 23, 42, .94)',
|
|
|
|
|
+ borderWidth: 0,
|
|
|
|
|
+ textStyle: { color: '#f8fafc', fontSize: 12 },
|
|
|
|
|
+ // 测井图以深度为主轴。明确使用 y 轴触发,避免 NPHI 等重复值
|
|
|
|
|
+ // 被 ECharts 按相同 x 值一次性全部收进 tooltip。
|
|
|
|
|
+ axisPointer: { type: 'line', axis: 'y' },
|
|
|
|
|
+ formatter: (params: any) => {
|
|
|
|
|
+ const rows = Array.isArray(params) ? params : [params]
|
|
|
|
|
+ const uniqueRows = Array.from(
|
|
|
|
|
+ new Map(rows.map((item: any) => [item.seriesName, item])).values()
|
|
|
|
|
+ ) as any[]
|
|
|
|
|
+ const depth = uniqueRows[0]?.value?.[1]
|
|
|
|
|
+ const content = uniqueRows
|
|
|
|
|
+ .map((item: any) => {
|
|
|
|
|
+ const curve = curves.find((entry) => entry.name === item.seriesName)
|
|
|
|
|
+ const value = item.value?.[0]
|
|
|
|
|
+ return `<div style="display:flex;justify-content:space-between;gap:24px;margin-top:5px"><span><i style="display:inline-block;width:8px;height:8px;border-radius:50%;background:${item.color};margin-right:7px"></i>${item.seriesName}</span><b>${formatNumber(value)} ${curve?.unit ?? ''}</b></div>`
|
|
|
|
|
+ })
|
|
|
|
|
+ .join('')
|
|
|
|
|
+ return `<div style="font-weight:600;margin-bottom:3px">深度 ${formatNumber(depth)} m</div>${content}`
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ dataZoom: [
|
|
|
|
|
+ {
|
|
|
|
|
+ type: 'inside',
|
|
|
|
|
+ yAxisIndex: [0, 1, 2, 3],
|
|
|
|
|
+ filterMode: 'none',
|
|
|
|
|
+ zoomOnMouseWheel: true,
|
|
|
|
|
+ moveOnMouseMove: true,
|
|
|
|
|
+ moveOnMouseWheel: false
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ type: 'slider',
|
|
|
|
|
+ yAxisIndex: [0, 1, 2, 3],
|
|
|
|
|
+ filterMode: 'none',
|
|
|
|
|
+ right: 4,
|
|
|
|
|
+ top: 8,
|
|
|
|
|
+ bottom: 32,
|
|
|
|
|
+ width: 14,
|
|
|
|
|
+ showDetail: false,
|
|
|
|
|
+ borderColor: '#cbd5e1',
|
|
|
|
|
+ fillerColor: 'rgba(37, 99, 235, .14)',
|
|
|
|
|
+ handleSize: 14
|
|
|
|
|
+ }
|
|
|
|
|
+ ]
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function renderChart() {
|
|
|
|
|
+ if (!chartRef.value) return
|
|
|
|
|
+ if (!chart.value) chart.value = echarts.init(chartRef.value)
|
|
|
|
|
+ chart.value.setOption(buildChartOption(), true)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function updateSampling() {
|
|
|
|
|
+ sampledIndices.value = createDepthSampleIndices(dataset.value?.depths ?? [], SAMPLE_LIMIT)
|
|
|
|
|
+ nextTick(renderChart)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function loadWellData(wellId?: number) {
|
|
|
|
|
+ if (!wellId) return
|
|
|
|
|
+ dataLoading.value = true
|
|
|
|
|
+ try {
|
|
|
|
|
+ dataset.value = await DigitalApi.getWellLogDataset(wellId)
|
|
|
|
|
+ updateSampling()
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ dataset.value = undefined
|
|
|
|
|
+ sampledIndices.value = []
|
|
|
|
|
+ renderChart()
|
|
|
|
|
+ message.error('测井数据加载失败,请稍后重试')
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ dataLoading.value = false
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function loadWells() {
|
|
|
|
|
+ listLoading.value = true
|
|
|
|
|
+ let initialWellId: number | undefined
|
|
|
|
|
+ try {
|
|
|
|
|
+ const result = await DigitalApi.getWellList()
|
|
|
|
|
+ wells.value = result?.list ?? []
|
|
|
|
|
+ if (!selectedWellId.value && wells.value.length) selectedWellId.value = wells.value[0].id
|
|
|
|
|
+ initialWellId = selectedWellId.value
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ message.error('井列表加载失败,请稍后重试')
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ listLoading.value = false
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (initialWellId) await loadWellData(initialWellId)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function selectWell(well: DigitalWell) {
|
|
|
|
|
+ if (well.id === selectedWellId.value) return
|
|
|
|
|
+ selectedWellId.value = well.id
|
|
|
|
|
+ loadWellData(well.id)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+onMounted(async () => {
|
|
|
|
|
+ resizeObserver = new ResizeObserver(() => chart.value?.resize())
|
|
|
|
|
+ if (chartRef.value) resizeObserver.observe(chartRef.value)
|
|
|
|
|
+ renderChart()
|
|
|
|
|
+ await loadWells()
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+onBeforeUnmount(() => {
|
|
|
|
|
+ resizeObserver?.disconnect()
|
|
|
|
|
+ chart.value?.dispose()
|
|
|
|
|
+})
|
|
|
|
|
+</script>
|
|
|
|
|
+
|
|
|
|
|
+<template>
|
|
|
|
|
+ <div
|
|
|
|
|
+ class="digital-shell h-[calc(100vh-20px-var(--top-tool-height)-var(--tags-view-height)-var(--app-footer-height))]">
|
|
|
|
|
+ <aside class="well-panel">
|
|
|
|
|
+ <div class="panel-title">
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <div class="eyebrow">WELL DIRECTORY</div>
|
|
|
|
|
+ <h2>井目录</h2>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <span class="well-count">{{ wells.length }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <el-input v-model="keyword" :prefix-icon="Search" clearable placeholder="搜索井名 / UWI" />
|
|
|
|
|
+
|
|
|
|
|
+ <div v-loading="listLoading" class="well-list">
|
|
|
|
|
+ <button
|
|
|
|
|
+ v-for="well in filteredWells"
|
|
|
|
|
+ :key="well.id"
|
|
|
|
|
+ type="button"
|
|
|
|
|
+ class="well-item"
|
|
|
|
|
+ :class="{ active: selectedWellId === well.id }"
|
|
|
|
|
+ @click="selectWell(well)">
|
|
|
|
|
+ <span class="well-status"></span>
|
|
|
|
|
+ <span class="well-text">
|
|
|
|
|
+ <strong>{{ getWellName(well) }}</strong>
|
|
|
|
|
+ <small>{{ well.uwi || `ID ${well.id}` }}</small>
|
|
|
|
|
+ </span>
|
|
|
|
|
+ <span class="well-depth">{{ well.stop ? `${well.stop.toFixed(0)}m` : '' }}</span>
|
|
|
|
|
+ </button>
|
|
|
|
|
+ <el-empty
|
|
|
|
|
+ v-if="!listLoading && !filteredWells.length"
|
|
|
|
|
+ :image-size="70"
|
|
|
|
|
+ description="暂无匹配井" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div v-if="selectedWell" class="well-summary">
|
|
|
|
|
+ <div class="summary-caption">当前井信息</div>
|
|
|
|
|
+ <dl>
|
|
|
|
|
+ <div
|
|
|
|
|
+ ><dt>井名</dt><dd>{{ getWellName(selectedWell) }}</dd></div
|
|
|
|
|
+ >
|
|
|
|
|
+ <div
|
|
|
|
|
+ ><dt>UWI</dt><dd>{{ selectedWell.uwi || '--' }}</dd></div
|
|
|
|
|
+ >
|
|
|
|
|
+ <div
|
|
|
|
|
+ ><dt>深度段</dt><dd>{{ depthRange }}</dd></div
|
|
|
|
|
+ >
|
|
|
|
|
+ <div
|
|
|
|
|
+ ><dt>采样间隔</dt><dd>{{ selectedWell.step ?? '--' }} m</dd></div
|
|
|
|
|
+ >
|
|
|
|
|
+ </dl>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </aside>
|
|
|
|
|
+
|
|
|
|
|
+ <main class="workspace">
|
|
|
|
|
+ <section v-loading="dataLoading" class="log-board">
|
|
|
|
|
+ <div class="track-header">
|
|
|
|
|
+ <div v-for="(group, track) in trackCurves" :key="track" class="track-card">
|
|
|
|
|
+ <div v-for="curve in group" :key="curve.key" class="curve-scale">
|
|
|
|
|
+ <div class="curve-name" :style="{ color: curve.color }">
|
|
|
|
|
+ <i :style="{ backgroundColor: curve.color }"></i>{{ curve.name }}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="scale-row"
|
|
|
|
|
+ ><span>{{ curve.inverse ? curve.max : curve.min }}</span
|
|
|
|
|
+ ><b>{{ curve.unit }}</b
|
|
|
|
|
+ ><span>{{ curve.inverse ? curve.min : curve.max }}</span></div
|
|
|
|
|
+ >
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="depth-caption">DEPTH<br /><span>m</span></div>
|
|
|
|
|
+ <div ref="chartRef" class="log-chart"></div>
|
|
|
|
|
+ <el-empty
|
|
|
|
|
+ v-if="!dataLoading && !originalCount"
|
|
|
|
|
+ class="chart-empty"
|
|
|
|
|
+ description="请选择有测井数据的井" />
|
|
|
|
|
+ </section>
|
|
|
|
|
+ </main>
|
|
|
|
|
+ </div>
|
|
|
|
|
+</template>
|
|
|
|
|
+
|
|
|
|
|
+<style scoped lang="scss">
|
|
|
|
|
+.digital-shell {
|
|
|
|
|
+ --navy: #0f2742;
|
|
|
|
|
+
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ color: #1e293b;
|
|
|
|
|
+ background: #e8edf3;
|
|
|
|
|
+ border: 1px solid #cbd5e1;
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.eyebrow {
|
|
|
|
|
+ font-size: 10px;
|
|
|
|
|
+ font-weight: 700;
|
|
|
|
|
+ letter-spacing: 1.5px;
|
|
|
|
|
+ color: #64748b;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.well-panel {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex: 0 0 238px;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ gap: 14px;
|
|
|
|
|
+ padding: 18px 14px;
|
|
|
|
|
+ background: #f8fafc;
|
|
|
|
|
+ border-right: 1px solid #cbd5e1;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.panel-title {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.panel-title h2 {
|
|
|
|
|
+ margin: 3px 0 0;
|
|
|
|
|
+ font-size: 20px;
|
|
|
|
|
+ color: var(--navy);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.well-count {
|
|
|
|
|
+ min-width: 30px;
|
|
|
|
|
+ padding: 5px 8px;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ font-weight: 700;
|
|
|
|
|
+ color: #fff;
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+ background: var(--navy);
|
|
|
|
|
+ border-radius: 12px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.well-list {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ min-height: 0;
|
|
|
|
|
+ padding-right: 3px;
|
|
|
|
|
+ overflow-y: auto;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.well-item {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ padding: 10px;
|
|
|
|
|
+ margin-bottom: 4px;
|
|
|
|
|
+ color: #334155;
|
|
|
|
|
+ text-align: left;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ background: transparent;
|
|
|
|
|
+ border: 0;
|
|
|
|
|
+ border-radius: 6px;
|
|
|
|
|
+ transition: 0.15s ease;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 9px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.well-item:hover {
|
|
|
|
|
+ background: #e9eff6;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.well-item.active {
|
|
|
|
|
+ color: #0f3d69;
|
|
|
|
|
+ background: #dce9f6;
|
|
|
|
|
+ box-shadow: inset 3px 0 #2563eb;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.well-status {
|
|
|
|
|
+ width: 7px;
|
|
|
|
|
+ height: 7px;
|
|
|
|
|
+ flex: none;
|
|
|
|
|
+ background: #22c55e;
|
|
|
|
|
+ border-radius: 50%;
|
|
|
|
|
+ box-shadow: 0 0 0 3px rgb(34 197 94 / 12%);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.well-text {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ min-width: 0;
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.well-text strong {
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+ text-overflow: ellipsis;
|
|
|
|
|
+ white-space: nowrap;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.well-text small {
|
|
|
|
|
+ margin-top: 2px;
|
|
|
|
|
+ font-size: 10px;
|
|
|
|
|
+ color: #94a3b8;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.well-depth {
|
|
|
|
|
+ font-family: monospace;
|
|
|
|
|
+ font-size: 10px;
|
|
|
|
|
+ color: #64748b;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.well-summary {
|
|
|
|
|
+ padding: 13px;
|
|
|
|
|
+ background: #eef3f8;
|
|
|
|
|
+ border: 1px solid #d8e0e9;
|
|
|
|
|
+ border-radius: 6px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.summary-caption {
|
|
|
|
|
+ margin-bottom: 8px;
|
|
|
|
|
+ font-size: 10px;
|
|
|
|
|
+ font-weight: 700;
|
|
|
|
|
+ letter-spacing: 1px;
|
|
|
|
|
+ color: #64748b;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.well-summary dl {
|
|
|
|
|
+ margin: 0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.well-summary dl div {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ gap: 10px;
|
|
|
|
|
+ padding: 4px 0;
|
|
|
|
|
+ font-size: 11px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.well-summary dt {
|
|
|
|
|
+ color: #64748b;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.well-summary dd {
|
|
|
|
|
+ margin: 0;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: #1e293b;
|
|
|
|
|
+ text-overflow: ellipsis;
|
|
|
|
|
+ white-space: nowrap;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.workspace {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ min-width: 0;
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ padding: 14px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.log-board {
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ min-height: 0;
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ background: #fff;
|
|
|
|
|
+ border: 1px solid #aebbc9;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.track-header {
|
|
|
|
|
+ display: grid;
|
|
|
|
|
+ flex: none;
|
|
|
|
|
+ grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
|
|
|
+ gap: 1.15%;
|
|
|
|
|
+ margin: 0 6% 0 7%;
|
|
|
|
|
+ background: #64748b;
|
|
|
|
|
+ border-right: 1px solid #64748b;
|
|
|
|
|
+ border-left: 1px solid #64748b;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.track-card {
|
|
|
|
|
+ min-width: 0;
|
|
|
|
|
+ background: #fff;
|
|
|
|
|
+ border-top: 1px solid #64748b;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.curve-scale {
|
|
|
|
|
+ padding: 5px 7px 6px;
|
|
|
|
|
+ border-bottom: 1px dotted #cbd5e1;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.curve-name {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+ font-weight: 800;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.curve-name i {
|
|
|
|
|
+ width: 18px;
|
|
|
|
|
+ height: 2px;
|
|
|
|
|
+ margin-right: 6px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.scale-row {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ margin-top: 3px;
|
|
|
|
|
+ font-family: monospace;
|
|
|
|
|
+ font-size: 11px;
|
|
|
|
|
+ color: #334155;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.scale-row b {
|
|
|
|
|
+ font-weight: 500;
|
|
|
|
|
+ color: #64748b;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.depth-caption {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ top: 9px;
|
|
|
|
|
+ left: 1%;
|
|
|
|
|
+ z-index: 2;
|
|
|
|
|
+ width: 5%;
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+ font-weight: 800;
|
|
|
|
|
+ line-height: 1.4;
|
|
|
|
|
+ color: #334155;
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.depth-caption span {
|
|
|
|
|
+ font-size: 11px;
|
|
|
|
|
+ font-weight: 500;
|
|
|
|
|
+ color: #64748b;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.log-chart {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ min-height: 360px;
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.chart-empty {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ inset: 150px 0 0;
|
|
|
|
|
+ background: rgb(255 255 255 / 85%);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+@media (width <= 1200px) {
|
|
|
|
|
+ .well-panel {
|
|
|
|
|
+ flex-basis: 205px;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+</style>
|