Files
mes-ui-d2/src/views/data-platform/traceability/backward/index.vue
sheng 3c4a665dc3
Some checks failed
Release pipeline / publish (push) Has been cancelled
Release pipeline / Always run job (push) Has been cancelled
修复反向追溯图谱迁移问题
2026-06-22 16:57:37 +08:00

408 lines
13 KiB
Vue

<template>
<d2-container>
<template #header>
<div class="search-bar">
<el-form ref="form" :inline="true" :model="form" size="mini">
<el-form-item :label="$t(key('battery_id'))" prop="battery_id">
<el-input
v-model="form.battery_id"
:placeholder="$t(key('enter_battery_id'))"
clearable
style="width:260px"
@keyup.enter.native="fetchData"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" :disabled="loading" :loading="loading" @click="fetchData">
{{ $t(key('query')) }}
</el-button>
<el-button icon="el-icon-download" :disabled="loading" @click="exportTree">
{{ $t(key('export')) }}
</el-button>
<el-button icon="el-icon-refresh" :disabled="loading" @click="resetForm">
{{ $t(key('reset')) }}
</el-button>
</el-form-item>
</el-form>
</div>
</template>
<div v-loading="loading" class="traceability-page">
<el-empty v-if="!hasGraph" :description="$t(key('enter_battery_id'))" />
<RelationGraph
v-else
ref="graphRef"
class="trace-graph"
:options="currentGraphOptions"
:on-node-click="onNodeClick"
>
<template #graph-plug>
<div class="trace-panel">
<div class="trace-panel__label">{{ $t(key('view_type')) }}:</div>
<el-radio-group v-model="currentCase" size="small" @change="renderGraph">
<el-radio-button label="horizontal">{{ $t(key('horizontal')) }}</el-radio-button>
<el-radio-button label="vertical">{{ $t(key('vertical')) }}</el-radio-button>
</el-radio-group>
<div class="trace-panel__label trace-panel__label--spaced">{{ $t(key('position')) }}:</div>
<div class="trace-panel__row">
<el-input v-model.trim="locator.item_code" size="small" clearable :placeholder="$t(key('enter_item_code'))" />
<el-button size="small" type="primary" @click="locateNode('item_code', locator.item_code)">
{{ $t(key('confirm_position')) }}
</el-button>
</div>
<div class="trace-panel__row">
<el-input v-model.trim="locator.item_batch" size="small" clearable :placeholder="$t(key('enter_item_batch'))" />
<el-button size="small" type="primary" @click="locateNode('item_batch', locator.item_batch)">
{{ $t(key('confirm_position')) }}
</el-button>
</div>
<div class="trace-panel__row">
<el-input
v-model.trim="locator.workingsubclass_code"
size="small"
clearable
:placeholder="$t(key('enter_work_unit'))"
/>
<el-button size="small" type="primary" @click="locateNode('workingsubclass_code', locator.workingsubclass_code, true)">
{{ $t(key('confirm_position')) }}
</el-button>
</div>
</div>
</template>
<template #node="{ node }">
<div class="trace-node" :class="{ 'is-active': activeNodeId === String(node.id) }">
<div class="trace-node__tag">
<el-tag type="warning" effect="dark" size="small">{{ node.data.type_name || '-' }}</el-tag>
</div>
<div class="trace-node__title">{{ node.text || node.id }}</div>
<div class="trace-node__body">
<div class="trace-node__line">{{ $t(key('item_name')) }}: {{ node.data.item_name || '-' }}</div>
<div class="trace-node__line">{{ $t(key('item_code')) }}: {{ node.data.item_code || '-' }}</div>
<div class="trace-node__line">{{ $t(key('item_batch')) }}: {{ node.data.item_batch || '-' }}</div>
<div class="trace-node__line">{{ $t(key('work_unit')) }}: {{ node.data.workingsubclass_code || '-' }}</div>
<div class="trace-node__line">{{ $t(key('work_unit_name')) }}: {{ node.data.workingsubclass_name || '-' }}</div>
<div class="trace-node__line">{{ $t(key('process_code')) }}: {{ node.data.process_code || '-' }}</div>
<div class="trace-node__line">{{ $t(key('start_time')) }}: {{ node.data.start_time || '-' }}</div>
<div class="trace-node__line">{{ $t(key('finish_time')) }}: {{ node.data.finish_time || '-' }}</div>
<div class="trace-node__line">{{ $t(key('inspection_person')) }}: {{ node.data.inspection_person || '-' }}</div>
<div class="trace-node__line">{{ $t(key('inspection_time')) }}: {{ node.data.inspection_time || '-' }}</div>
<div class="trace-node__line">{{ $t(key('quality_person')) }}: {{ node.data.quality_person || '-' }}</div>
<div class="trace-node__line">{{ $t(key('quality_time')) }}: {{ node.data.quality_time || '-' }}</div>
<div class="trace-node__line">{{ $t(key('device_name')) }}: {{ node.data.device_name || '-' }}</div>
<div class="trace-node__line">{{ $t(key('device_code')) }}: {{ node.data.device_code || '-' }}</div>
</div>
</div>
</template>
</RelationGraph>
</div>
</d2-container>
</template>
<script>
import RelationGraph from 'relation-graph'
import { i18nMixin } from '@/composables/useI18n'
import {
exportBackwardTraceabilityTree,
getBackwardTraceabilityData
} from '@/api/data-platform/traceability/backward'
export default {
name: 'data-platform-traceability-backward',
components: {
RelationGraph
},
mixins: [i18nMixin('page.data_platform.traceability.backward')],
data () {
return {
loading: false,
form: {
battery_id: this.$route.params.battery_id || ''
},
traceData: {},
currentCase: 'horizontal',
activeNodeId: '',
locator: {
item_code: '',
item_batch: '',
workingsubclass_code: ''
}
}
},
computed: {
graph () {
return this.traceData.relation_graph || {}
},
hasGraph () {
return Array.isArray(this.graph.nodes) && this.graph.nodes.length > 0
},
horizontalOptions () {
return {
backgroundImageNoRepeat: true,
defaultNodeBorderWidth: 0,
defaultLineShape: 3,
defaultNodeShape: 0,
defaultJunctionPoint: 'tb',
defaultNodeWidth: 330,
defaultNodeHeight: 430,
defaultFocusRootNode: false,
downloadImageFileName: this.downloadName,
defaultLineColor: 'rgba(0, 186, 189, 1)',
defaultNodeColor: 'rgba(0, 206, 209, 1)',
moveToCenterWhenResize: true,
defaultLineMarker: {
markerWidth: 12,
markerHeight: 12,
refX: '10',
refY: 6,
data: 'M2,2 L10,6 L2,10 L6,6 L2,2'
},
layouts: [
{
label: this.$t(this.key('center')),
layoutName: 'tree',
layoutClassName: 'seeks-layout-center',
centerOffset_x: 0,
centerOffset_y: 0,
distance_coefficient: 1,
layoutDirection: 'v',
from: 'top',
min_per_width: 560,
max_per_width: 1100,
min_per_height: 560,
max_per_height: 1100
}
]
}
},
verticalOptions () {
return {
defaultNodeShape: 1,
defaultNodeBorderWidth: 0,
defaultNodeWidth: 330,
defaultNodeHeight: 430,
defaultLineShape: 3,
defaultJunctionPoint: 'lr',
defaultFocusRootNode: false,
downloadImageFileName: this.downloadName,
defaultLineColor: 'rgba(0, 186, 189, 1)',
defaultNodeColor: 'rgba(0, 206, 209, 1)',
moveToCenterWhenResize: true,
layout: {
label: this.$t(this.key('center')),
layoutName: 'tree',
layoutClassName: 'seeks-layout-center',
defaultNodeShape: 0,
defaultLineShape: 1,
from: 'left',
min_per_width: 660,
max_per_width: 1100,
min_per_height: 560,
max_per_height: 700
},
defaultLineMarker: {
markerWidth: 12,
markerHeight: 12,
refX: 6,
refY: 6,
data: 'M2,2 L10,6 L2,10 L6,6 L2,2'
}
}
},
currentGraphOptions () {
return this.currentCase === 'horizontal' ? this.horizontalOptions : this.verticalOptions
},
downloadName () {
const firstNode = this.graph.nodes && this.graph.nodes[0]
return firstNode && firstNode.text ? firstNode.text : this.$t(this.key('reverse'))
}
},
mounted () {
if (this.form.battery_id) this.fetchData()
},
methods: {
ensureBatteryInput () {
if (this.form.battery_id) return true
this.$message.error(this.$t(this.key('enter_battery_id')))
return false
},
async fetchData () {
if (!this.ensureBatteryInput()) return
this.loading = true
try {
const res = await getBackwardTraceabilityData({ ...this.form })
const data = res && res.data ? res.data : res
this.traceData = data || {}
const firstNode = this.graph.nodes && this.graph.nodes[0]
this.activeNodeId = firstNode ? String(firstNode.id) : ''
this.$nextTick(this.renderGraph)
} finally {
this.loading = false
}
},
resetForm () {
this.form.battery_id = ''
this.traceData = {}
this.activeNodeId = ''
this.locator = {
item_code: '',
item_batch: '',
workingsubclass_code: ''
}
},
renderGraph () {
if (!this.hasGraph || !this.$refs.graphRef) return
this.$refs.graphRef.setOptions(this.currentGraphOptions)
this.$refs.graphRef.setJsonData(this.graph)
},
onNodeClick (nodeObject) {
this.activeNodeId = String(nodeObject.id)
},
locateNode (field, value, onlyFinalProduct = false) {
if (!value || !this.hasGraph) return
const target = this.graph.nodes.find(node => {
const data = node.data || {}
if (String(data[field] || '') !== String(value)) return false
if (!onlyFinalProduct) return true
return data.type_name === '制成品' && Number(data.status) === -1
})
if (!target) {
this.$message.warning(this.$t(this.key('node_not_found')))
return
}
this.activeNodeId = String(target.id)
this.$nextTick(() => {
const graphInstance = this.$refs.graphRef && this.$refs.graphRef.getInstance()
if (!graphInstance) return
graphInstance.setZoom(200)
graphInstance.focusNodeById(target.id)
})
},
async exportTree () {
if (!this.ensureBatteryInput()) return
if (!this.hasGraph) {
this.$message.warning(this.$t(this.key('query_before_export')))
return
}
const res = await exportBackwardTraceabilityTree({
...this.form,
tree_list: JSON.stringify(this.graph.nodes || [])
})
const data = res && res.data ? res.data : res
this.downloadExport(data)
},
downloadExport (url) {
if (!url) return
const link = document.createElement('a')
link.href = url
link.download = `${this.form.battery_id}${this.$t(this.key('reverse'))}`
link.target = '_blank'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
}
}
}
</script>
<style lang="scss" scoped>
.search-bar {
margin-bottom: -18px;
}
.traceability-page {
height: calc(100vh - 170px);
min-height: 620px;
}
.trace-graph {
width: 100%;
height: 100%;
min-height: 620px;
border: 1px solid #ebeef5;
background: #f7f9fb;
}
.trace-panel {
position: absolute;
top: 12px;
left: 12px;
z-index: 20;
width: 360px;
padding: 12px;
border: 1px solid #dcdfe6;
border-radius: 4px;
background: rgba(255, 255, 255, 0.94);
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.08);
}
.trace-panel__label {
margin-bottom: 8px;
color: #606266;
font-size: 13px;
font-weight: 600;
}
.trace-panel__label--spaced {
margin-top: 14px;
}
.trace-panel__row {
display: grid;
grid-template-columns: minmax(0, 1fr) 92px;
gap: 8px;
margin-top: 10px;
}
.trace-node {
position: relative;
width: 330px;
height: 410px;
padding: 28px 12px 12px;
color: #303133;
cursor: pointer;
text-align: left;
}
.trace-node.is-active .trace-node__body {
border-color: #f56c6c;
box-shadow: 0 0 0 2px rgba(245, 108, 108, 0.18);
}
.trace-node__tag {
position: absolute;
top: 0;
left: 0;
}
.trace-node__title {
min-height: 22px;
margin-bottom: 8px;
padding-right: 8px;
font-size: 16px;
font-weight: 600;
line-height: 1.4;
word-break: break-word;
}
.trace-node__body {
height: 340px;
overflow: auto;
padding: 10px;
border: 1px solid transparent;
border-radius: 8px;
background: #fff;
color: #555;
}
.trace-node__line {
min-height: 24px;
padding: 4px 0;
border-bottom: 1px solid #efefef;
line-height: 1.4;
white-space: normal;
word-break: break-all;
}
</style>