<script setup>
import {computed, ref, nextTick, watch} from 'vue'
import { useI18n } from 'vue-i18n'
import * as XLSX from 'xlsx/xlsx.mjs'
import { useAppStore } from '@/stores/app.js'
import { useThemeStore } from '@/stores/theme.js'
import { useWellStore } from '@/stores/well.js'
import Card from '@/components/layout/Card.vue'
import WellTrajectory from '@/components/BoetWidgets/3d/WellTrajectory.vue'
import ImportDialog from './import-dialog/ImportDialog.vue'
import InterpolateDialog from './interpolate-dialog/InterpolateDialog.vue'
import TrackPrediction from './track-prediction/TrackPrediction.vue'
import WaitDrillTrackDesignDialog from './wait-drill-track-design-dialog/WaitDrillTrackDesignDialog.vue'
import HitTargetPredictionDialog from './hit-target-prediction-dialog/HitTargetPredictionDialog.vue'
import EditDialog from './edit-dialog/EditDialog.vue'
import {deepClone, exportExcel, formatTimeString, formatDecimal} from '@/common/util.js'
import BaseFrame from '@/components/layout/BaseFrame.vue'
import {Plus, Check, Close, Delete, Download, DataLine, Aim, EditPen, Share, Upload} from '@element-plus/icons-vue'
import {ElLoading, ElMessage} from 'element-plus'
import {getDrilledTracks, deleteDrilledTrack, getDrilledTrackDatas, saveDrilledTrackData, deleteDrilledTrackData} from '@/api/modules/drilled-track.js'
const { t } = useI18n()
const appStore = useAppStore()
const themeStore = useThemeStore()
const wellStore = useWellStore()
const well = computed(()=>{
return wellStore.getWell()
})
const rowIndex = ref(null) // 表格行
const selectRow = ref(null) // 选中的行
const importDialogVisible = ref(false)
const interpolateDialogVisible = ref(false)
const waitDrillTrackDesignDialogVisible = ref(false)
const hitTargetPredictionDialogVisible = ref(false)
const isAdd = ref(true)
const editDialogVisible = ref(false)
const tableRef = ref(null)
const drilledTracks = ref([])
const importSource = ref('file') // 新增:默认是文件导入
// 实钻轨迹
const handleGetDrilledTracks = ()=>{
const params = {
wellId: well.value.wellId,
wellBoreId: well.value.wellBoreId
}
getDrilledTracks(params).then(res=>{
if (res) {
drilledTracks.value = res
// 使用 nextTick 确保表格已经渲染
nextTick(()=>{
// 确保表格引用存在且有数据
if (tableRef.value && drilledTracks.value.length) {
const firstRow = drilledTracks.value[0]
// 默认选中第一行
tableRef.value.setCurrentRow(firstRow)
}
})
}
})
}
// 点击行时,设置当前行
const currentRow = ref(null)
const handleCurrentChange = (val)=>{
currentRow.value = val
handleGetDrilledTrackDatas()
}
const drilledTrackDatas = ref([])
const drilledTrackGraphTempDatas = ref([])
// 实钻轨迹数据
const handleGetDrilledTrackDatas = ()=>{
const params = {
wellId: well.value.wellId,
wellBoreId: well.value.wellBoreId,
drilledTrackId: currentRow.value.id
}
getDrilledTrackDatas(params).then(res=>{
drilledTrackDatas.value = res
drilledTrackGraphTempDatas.value = res
})
}
// 删除待钻轨迹
const handleDeleteDrilledTrack = (idx, row)=>{
const params = {
wellId: row.wellId,
drilledTrackId: row.id
}
deleteDrilledTrack(params).then(()=>{
drilledTracks.value.splice(idx, 1)
if (drilledTracks.value.length) {
const firstRow = drilledTracks.value[0]
// 默认选中第一行
tableRef.value.setCurrentRow(firstRow)
} else {
drilledTrackDatas.value = []
}
})
}
// 新增
const handleAdd = (idx)=>{
// rowIndex.value = idx + 1
// drilledTrackDatas.value.splice(idx + 1, 0, {md: null, inc: null, azi: null})
const newIndex = idx + 1
drilledTrackDatas.value.splice(newIndex, 0, {md: null, inc: null, azi: null})
// 设置新行编辑状态
editingRows.value.push(newIndex)
selectRows.value[newIndex] = deepClone(drilledTrackDatas.value[newIndex])
}
// 编辑
const handleUpdate = (idx)=>{
if (!editingRows.value.includes(idx)) {
editingRows.value.push(idx)
selectRows.value[idx] = deepClone(drilledTrackDatas.value[idx])
}
}
// 取消编辑
const handleCancel = (idx, row)=>{
if (!row.id) { // 新增行取消
drilledTrackDatas.value.splice(idx, 1)
}
// 从编辑状态中移除
const indexInEditing = editingRows.value.indexOf(idx)
if (indexInEditing !== -1) {
editingRows.value.splice(indexInEditing, 1)
delete selectRows.value[idx]
}
}
// 保存数据
const handleComplete = async(idx)=>{
const row = drilledTrackDatas.value[idx]
// 获取相邻行的值(考虑编辑状态)
const prevRow = idx > 0 ? drilledTrackDatas.value[idx - 1] : null
const nextRow = idx < drilledTrackDatas.value.length - 1 ? drilledTrackDatas.value[idx + 1] : null
// 两个深度之间的间隔不能小于这个值
const eps = 1e-8
// 验证测深值必须大于上一行
if (prevRow && prevRow.md !== null && row.md - prevRow.md <= eps) {
ElMessage.warning('测深必须大于上一个值')
return false
}
// 验证测深值必须小于下一行
if (nextRow && nextRow.md !== null && row.md - nextRow.md >= eps) {
ElMessage.warning('测深必须小于下一个值')
return false
}
const nonEmptyNewRows = drilledTrackDatas.value
.slice(idx + 1) // 获取当前行之后的行
.filter(r=>!r.id && // 新增行(没有ID)
editingRows.value.includes(drilledTrackDatas.value.indexOf(r)) && // 处于编辑状态
(r.md !== null || r.inc !== null || r.azi !== null) // 至少有一个字段非空
)
.map(r=>({ ...r })) // 深拷贝
// console.log(nonEmptyNewRows)
const postData = {
id: row.id,
wellId: well.value.wellId,
wellBoreId: well.value.wellBoreId,
drilledTrackId: currentRow.value.id,
md: row.md,
inc: row.inc,
azi: row.azi
}
try {
await saveDrilledTrackData(postData)
// 从编辑状态中移除
const indexInEditing = editingRows.value.indexOf(idx)
// console.log(indexInEditing)
if (indexInEditing !== -1) {
editingRows.value.splice(indexInEditing, 1)
delete selectRows.value[idx]
}
// 重新加载数据
await handleGetDrilledTrackDatas()
// +++ 新增:重新添加非空的新行并恢复编辑状态 +++
nonEmptyNewRows.forEach(newRow=>{
console.log(nonEmptyNewRows)
const newIndex = drilledTrackDatas.value.length - 1
console.log(newIndex)
console.log(drilledTrackDatas)
drilledTrackDatas.value.push(newRow)
console.log(newRow)
editingRows.value.push(newIndex)
console.log(editingRows.value.push(newIndex))
selectRows.value[newIndex] = deepClone(newRow)
console.log(selectRows)
})
} catch (error) {
console.error('保存失败:', error)
ElMessage.error('保存失败')
}
}
// 删除轨迹数据
const handleDeleteDrilledTrackData = (row)=>{
const params = {
wellId: well.value.wellId,
drilledTrackDataId: row.id
}
deleteDrilledTrackData(params).then(()=>{
handleGetDrilledTrackDatas()
})
}
// 3D轨迹配置
const drilledTrackGraphConfig = ref({
skin: { blackTheme: themeStore.isDark},
showLegend: false,
showScaleSelector: false,
showTubeSizeSlider: true,
tubeSize: 0
})
// 图形数据
const drilledTrackGraphDatas = computed(()=>{
if (!drilledTrackGraphTempDatas.value) {
return null
}
// 过滤新录入的数据
const filterData = drilledTrackGraphTempDatas.value.filter(t=>t.id)
const data = filterData.map(({ ns: x, ew: y, ...rest })=>({
x,
y,
...rest
}))
return [
{
'code': 0,
'isDrilled': true,
'data': data,
'name': '实钻轨迹',
'isShow': true
}
]
})
const excelData = ref([])
const excelFile = ref(null)
const loading = ref(null)
// 导入轨迹数据
const handleImportDrilledTrackData = (ev)=>{
// 获取文件后缀
const ext = ev.name.substring(ev.name.lastIndexOf('.'))
if (ext !== '.xls' && ext !== '.xlsx') {
ElMessage.warning('请选择EXCEL文件')
return false
}
excelData.value = []
excelFile.value = ev.raw
if (!excelFile.value) {
ElMessage.warning('文件打开失败')
return false
} else {
loading.value = ElLoading.service({
lock: true,
text: '文件解析中,请稍候...',
background: 'rgba(0, 0, 0, 0.7)'
})
// 读取文件
setTimeout(readExcelFile, 100)
}
}
// 文件读取并解析
const readExcelFile = ()=>{
const reader = new FileReader()
reader.readAsBinaryString(excelFile.value)// 以二进制的方式读取
reader.onload = ev=>{
const fileData = ev.target.result
const workBook = XLSX.read(fileData, {type: 'binary'})// 解析二进制格式数据
workBook.SheetNames.forEach((sheetName, index)=>{
const item = {
sheetName: sheetName
}
const workSheet = workBook.Sheets[workBook.SheetNames[index]]// 获取第一个Sheet
item.rows = XLSX.utils.sheet_to_json(workSheet, {range: -1, defval: null})// 指定-1行为列头,空单元格赋值 null
// 获取json数据key
if (item.rows && item.rows.length > 0) {
item.keys = []
const keys = Object.keys(item.rows[0])
keys.forEach((key, index)=>{
item.keys.push({
idx: index,
key: key,
label: ''
})
})
}
excelData.value.push(item)
})
importDialogVisible.value = true
loading.value.close()
}
}
const handleImport = (source)=>{
if (source === 'file') {
// 模拟点击隐藏的 input 或 el-upload 打开系统文件夹
document.querySelector('#hiddenFileInput').click()
} else if (source === 'clipboard') {
importSource.value = 'clipboard'
importDialogVisible.value = true
}
}
const handleImportReload = ()=>{
handleGetDrilledTrackDatas()
importDialogVisible.value = false
}
// 导出轨迹数据
const handleExportDrilledTrackData = ()=>{
// 过滤数据,只取一部分属性的数据
const partData = drilledTrackDatas.value.map(({md, inc, azi, tvd, nsOffset, ewOffset, horiOffset, projOffset, dogleg})=>({
md,
inc,
azi,
tvd,
nsOffset,
ewOffset,
horiOffset,
projOffset,
dogleg
}))
// 值数组
const valueArray = partData.map(obj=>Object.values(obj))
const headerArray = ['测深', '井斜', '方位', '垂深', '南北位移', '东西位移', '水平位移', '投影位移', '狗腿度']
const sheetName = '实钻轨迹'
const fileName = '实钻轨迹.xlsx'
exportExcel(sheetName, headerArray, formatDecimal(valueArray), fileName, 15)
}
// 监听well变化
watch(()=>well, (newVal)=>{
if (Object.keys(newVal.value).length !== 0) {
handleGetDrilledTracks()
}
}, {deep: true, immediate: true})
const editingRows = ref([])
const selectRows = ref({})
// 修改:处理测深输入事件
const handleMdInput = (index)=>{
const row = drilledTrackDatas.value[index]
// 检查条件:当前是最后一行、正在编辑、测深有值、是新增行(无ID)
if (
index === drilledTrackDatas.value.length - 1 &&
editingRows.value.includes(index) &&
row.md !== null &&
row.md !== '' &&
!row.id
) {
// 在下方添加新行
drilledTrackDatas.value.push({md: null, inc: null, azi: null})
const newIndex = drilledTrackDatas.value.length - 1
// 设置新行进入编辑状态
editingRows.value.push(newIndex)
selectRows.value[newIndex] = deepClone(drilledTrackDatas.value[newIndex])
}
}
</script>
<template>
<BaseFrame>
<!-- 左侧内容区 -->
<div class="col-span-8 flex flex-1 flex-col gap-2">
<!-- 轨迹列表 -->
<Card>
<template #header>
<div class="flex justify-between pl-0 pr-0">
<div class="flex items-center gap-2">
<span class="text-slate-600 dark:text-slate-50 text-[14px]"><strong>【{{ well.wellId ? `${well.wellCode} | ${well.wellBoreName}` : '暂未选井' }}】</strong></span>
</div>
<div class="flex items-center">
<el-button :icon="Plus" size="small" class="mr-3" @click="editDialogVisible=true;isAdd=true;">{{ t('add') }}</el-button>
<el-dropdown @command="importSource=>handleImport(importSource)">
<el-button :icon="Download" size="small">{{ t('import') }}</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="file">导入文件</el-dropdown-item>
<el-dropdown-item command="clipboard">从剪切板导入</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<input
id="hiddenFileInput"
type="file"
accept=".xlsx,.xls"
style="display:none"
@change="handleImportDrilledTrackData"
>
<el-button :icon="Upload" size="small" class="ml-3" :disabled="!currentRow" @click="handleExportDrilledTrackData">
{{ t('export') }}
</el-button>
<el-button :icon="EditPen" size="small" :disabled="!currentRow" @click="interpolateDialogVisible=true">
{{ t('interpolate') }}
</el-button>
<el-button :icon="Share" size="small" :disabled="!currentRow" @click="appStore.toggleTrackPredictionDrawer()">
{{ t('plan') }}
</el-button>
<el-button :icon="Share" size="small" :disabled="!currentRow" @click="waitDrillTrackDesignDialogVisible=true">
待钻设计
</el-button>
<el-button :icon="Aim" size="small" :disabled="!currentRow" @click="hitTargetPredictionDialogVisible=true">
{{ t('targetEntry') }}
</el-button>
<el-button :icon="DataLine" size="small">
{{ t('trend') }}
</el-button>
</div>
</div>
</template>
<template #default>
<div class="h-[14vh]">
<el-table ref="tableRef" :data="drilledTracks" highlight-current-row height="100%" size="small" border stripe style="width: 100%;" @current-change="handleCurrentChange">
<el-table-column type="index" :label="t('order')" width="50" align="center" fixed />
<el-table-column prop="name" :label="t('name')" min-width="80" header-align="center" />
<el-table-column prop="createTime" align="center" :label="t('date')" width="120">
<template #default="{ row }">
<span>{{ formatTimeString(row.createTime, 'YYYY-MM-DD') }}</span>
</template>
</el-table-column>
<el-table-column fixed="right" align="center" :label="t('operate')" width="100">
<template #default="{$index,row}">
<el-button link type="success" size="small" @click.stop="editDialogVisible=true;isAdd=false;">
{{ t('revise') }}
</el-button>
<el-popconfirm
width="200"
class="box-item"
:title="`确认删除实钻轨迹?`"
placement="bottom"
confirm-button-text="确认"
cancel-button-text="取消"
@confirm="handleDeleteDrilledTrack($index,row)"
>
<template #reference>
<el-button link type="danger" size="small" @click.stop>
{{ t('delete') }}
</el-button>
</template>
</el-popconfirm>
</template>
</el-table-column>
</el-table>
</div>
</template>
</Card>
<!--/ 轨迹列表 -->
<!-- 详细数据 -->
<Card class="px-2" style="height:calc(100vh - 14rem)">
<div class="h-[98%] overflow-y-auto mt-2">
<el-table
size="small"
border
stripe
:data="formatDecimal(drilledTrackDatas)"
height="100%"
style="width: 100%"
>
<el-table-column :label="t('operate')" width="90" align="center" fixed="left">
<template #default="{$index,row}">
<template v-if="editingRows.includes($index)">
<div class="flex gap-1 *:!ml-0">
<el-button
size="small"
type="warning"
plain
:icon="Close"
@click="handleCancel($index, row)"
/>
<el-button
size="small"
plain
type="success"
:icon="Check"
@click="handleComplete($index)"
/>
</div>
</template>
<template v-else>
<div class="flex gap-1 *:!ml-0">
<el-button
size="small"
class="ml-1"
plain
:icon="Plus"
@click="handleAdd($index)"
/>
<el-popconfirm
width="200"
class="box-item"
:title="`确认删除本条轨迹数据?`"
placement="bottom"
confirm-button-text="确认"
cancel-button-text="取消"
@confirm="handleDeleteDrilledTrackData(row)"
>
<template #reference>
<el-button plain size="small" :icon="Delete" @click.stop />
</template>
</el-popconfirm>
</div>
</template>
</template>
</el-table-column>
<el-table-column label="测深" prop="depth" header-align="center" min-width="80" fixed="left">
<template #header>
<div>{{ t('Md') }}</div>
<div class="unit-size">(m)</div>
</template>
<template #default="{$index,row}">
<template v-if="editingRows.includes($index)">
<el-input
ref="input"
v-model="drilledTrackDatas[$index].md"
v-format-input
size="small"
placeholder="请输入测深"
@input="handleMdInput($index)"
/>
</template>
<span v-else @dblclick="handleUpdate($index)">{{ row.md }}</span>
</template>
</el-table-column>
<el-table-column label="井斜" prop="inc" header-align="center" min-width="80" fixed="left">
<template #header>
<div>{{ t('inclination') }}</div>
<div class="unit-size">(°)</div>
</template>
<template #default="{$index,row}">
<template v-if="editingRows.includes($index)">
<el-input
v-model="drilledTrackDatas[$index].inc"
v-format-input
size="small"
placeholder="请输入井斜"
/>
</template>
<span v-else @dblclick="handleUpdate($index)">{{ row.inc }}</span>
</template>
</el-table-column>
<el-table-column label="方位" prop="azi" header-align="center" min-width="80" fixed="left">
<template #header>
<div>{{ t('azimuth') }}</div>
<div class="unit-size">(°)</div>
</template>
<template #default="{$index,row}">
<template v-if="editingRows.includes($index)">
<el-input
v-model="drilledTrackDatas[$index].azi"
v-format-input
size="small"
placeholder="请输入方位"
/>
</template>
<span v-else @dblclick="handleUpdate($index)">{{ row.azi }}</span>
</template>
</el-table-column>
<el-table-column
label="垂深"
prop="tvd"
header-align="center"
min-width="80"
>
<template #header>
<div>{{ t('Tvd') }}</div>
<div class="unit-size">(m)</div>
</template>
</el-table-column>
<el-table-column
label="段长"
prop="sectionLength"
header-align="center"
min-width="80"
>
<template #header>
<div>{{ t('courseLength') }}</div>
<div class="unit-size">(m)</div>
</template>
</el-table-column>
<el-table-column
label="南北位移"
prop="nsOffset"
header-align="center"
min-width="80"
>
<template #header>
<div>{{ t('NsOffset') }}</div>
<div class="unit-size">(m)</div>
</template>
</el-table-column>
<el-table-column
label="东西位移"
prop="ewOffset"
header-align="center"
min-width="80"
>
<template #header>
<div>{{ t('EwOffset') }}</div>
<div class="unit-size">(m)</div>
</template>
</el-table-column>
<el-table-column
label="水平位移"
prop="horiOffset"
header-align="center"
min-width="80"
>
<template #header>
<div>{{ t('closure') }}</div>
<div class="unit-size">(m)</div>
</template>
</el-table-column>
<el-table-column
label="投影位移"
prop="projOffset"
header-align="center"
min-width="80"
>
<template #header>
<div>{{ t('Verticaldistance') }}</div>
<div class="unit-size">(m)</div>
</template>
</el-table-column>
<el-table-column
label="闭合方位"
prop="closeAzi"
header-align="center"
min-width="80"
>
<template #header>
<div>{{ t('closureAz') }}</div>
<div class="unit-size">(°)</div>
</template>
</el-table-column>
<el-table-column
label="狗腿度"
prop="dogleg"
header-align="center"
min-width="80"
>
<template #header>
<div>{{ t('dogleg') }}</div>
<div class="unit-size">(°/30m)</div>
</template>
</el-table-column>
<el-table-column
label="工具面"
prop="toolface"
header-align="center"
min-width="80"
>
<template #header>
<div>{{ t('toolface') }}</div>
<div class="unit-size">(°)</div>
</template>
</el-table-column>
<el-table-column
label="井斜变化率"
prop="incChangeRate"
header-align="center"
min-width="80"
>
<template #header>
<div>{{ t('incChangeRate') }}</div>
<div class="unit-size">(°/30m)</div>
</template>
</el-table-column>
<el-table-column
label="方位变化率"
prop="aziChangeRate"
header-align="center"
min-width="80"
>
<template #header>
<div>{{ t('aziChangeRate') }}</div>
<div class="unit-size">(°/30m)</div>
</template>
</el-table-column>
</el-table>
</div>
</Card>
<!--/ 详细数据 -->
</div>
<!-- 右侧2D/3D区域 -->
<div class="col-span-4 flex flex-col">
<Card class="flex-1">
<WellTrajectory
v-if="drilledTrackGraphDatas"
id="divTubeElement"
v-model:config="drilledTrackGraphConfig"
:show-well-name="false"
:show-full-screen-btn="true"
:show-animation="false"
is-auto-play.sync="true"
:data="drilledTrackGraphDatas"
/>
</Card>
</div>
<!-- 新增/修改 对话框 -->
<edit-dialog v-if="editDialogVisible" :row-data="isAdd?null:currentRow" @reload="handleImportReload" @close="editDialogVisible=false" />
<!-- 导入数据 -->
<import-dialog v-if="importDialogVisible" v-model:import-source="importSource" :drilled-track-id="currentRow.id" :excel-data="excelData" @reload="handleGetDrilledTrackDatas" @close="importDialogVisible=false" />
<!-- 插值计算 -->
<interpolate-dialog v-if="interpolateDialogVisible" :drilled-track-id="currentRow.id" @close="interpolateDialogVisible=false" />
<!-- 轨迹预测抽屉 -->
<el-drawer
v-model="appStore.trackPredictionDrawerVisible"
title="轨迹预测"
:with-header="true"
size="100%"
direction="ltr"
:close-on-click-modal="false"
:close-on-press-escape="false"
:destroy-on-close="true"
>
<track-prediction :last-drilled-track-data="drilledTrackDatas[drilledTrackDatas.length-1]" />
</el-drawer>
<!-- 待钻设计 -->
<wait-drill-track-design-dialog v-if="waitDrillTrackDesignDialogVisible" :last-drilled-track-data="drilledTrackDatas[drilledTrackDatas.length-1]" @close="waitDrillTrackDesignDialogVisible=false" />
<!-- 中靶预测 -->
<hit-target-prediction-dialog v-if="hitTargetPredictionDialogVisible" :drilled-track-id="currentRow.id" @close="hitTargetPredictionDialogVisible=false" />
</BaseFrame>
</template>
<style lang="scss" scoped>
.tab-content {
@apply p-2 absolute left-0 top-0 w-full h-full overflow-auto;
}
.border-card {
:deep(.el-tabs__nav-scroll) {
@apply flex;
.el-tabs__nav {
@apply flex-1;
.el-tabs__item {
@apply flex-1;
}
}
}
&.no-border {
@apply border-none;
}
}
:deep(.el-drawer) {
.el-drawer__header {
@apply mb-1
}
}
</style>
修改本代码,要求保存上一行时,自动新增行内有内容时应该保留当前行并保留编辑状态