This commit is contained in:
sheng
2026-06-22 16:35:23 +08:00
7 changed files with 483 additions and 3 deletions

View File

@@ -0,0 +1,27 @@
import { request } from '@/api/_service'
const BASE = 'data_middleground/basic_traceability/reverse_direction_traceability/'
function apiParams (method, data = {}) {
return {
method,
platform: 'background',
...data
}
}
export function getBackwardTraceabilityData (data) {
return request({
url: BASE + 'get_data',
method: 'get',
params: apiParams('data_middleground_basic_traceability_reverse_direction_traceability_get_data', data)
})
}
export function exportBackwardTraceabilityTree (data) {
return request({
url: BASE + 'export_tree',
method: 'post',
data: apiParams('data_middleground_basic_traceability_reverse_direction_traceability_export_tree', data)
})
}

View File

@@ -1255,6 +1255,44 @@
"accumulated_detail": "{device_name} ({device_code}): 10 Cumulative Error Details",
"workstation_detail": "Workstation [{device_name}] Channel Error Details"
}
},
"data_platform": {
"traceability": {
"backward": {
"query": "Search",
"reset": "Reset",
"export": "Export",
"battery_id": "Battery Barcode",
"enter_battery_id": "Enter battery barcode",
"view_type": "Graph Layout",
"horizontal": "Horizontal",
"vertical": "Vertical",
"position": "Locate",
"enter_item_code": "Enter material code",
"enter_item_batch": "Enter material batch",
"enter_work_unit": "Enter process unit",
"item_name": "Material Name",
"item_code": "Material Code",
"item_batch": "Material Batch",
"work_unit": "Process Unit",
"work_unit_name": "Process Unit Name",
"process_code": "Process Code",
"operator": "Material Handler",
"start_time": "Feeding Time",
"finish_time": "Completion Time",
"confirm_position": "Confirm Location",
"reverse": "Reverse Traceability",
"center": "Center",
"inspection_person": "Inspector",
"inspection_time": "Inspection Time",
"quality_person": "Quality Reviewer",
"quality_time": "Quality Review Time",
"device_name": "Device Name",
"device_code": "Device Code",
"node_not_found": "No matching node found",
"query_before_export": "Query traceability data before export"
}
}
}
},
"__MENU_TEMP_BEGIN__": "===== 以下为临时菜单翻译,后续统一删除 =====",

View File

@@ -1255,6 +1255,44 @@
"accumulated_detail": "{device_name}({device_code}) 10次累计通道异常详情",
"workstation_detail": "【{device_name}】工作站设备通道异常详情"
}
},
"data_platform": {
"traceability": {
"backward": {
"query": "查询",
"reset": "重置",
"export": "导出",
"battery_id": "电池条码",
"enter_battery_id": "请输入电池条码",
"view_type": "图谱形态",
"horizontal": "横向图谱",
"vertical": "纵向图谱",
"position": "定位",
"enter_item_code": "请输入物料编码",
"enter_item_batch": "请输入物料批次",
"enter_work_unit": "请输入工序单元",
"item_name": "物料名称",
"item_code": "物料编码",
"item_batch": "物料批次",
"work_unit": "工序单元",
"work_unit_name": "工序单元名称",
"process_code": "工序编码",
"operator": "卸料人",
"start_time": "投料时间",
"finish_time": "完成时间",
"confirm_position": "定位确认",
"reverse": "反向追溯",
"center": "中心",
"inspection_person": "点检人员",
"inspection_time": "点检时间",
"quality_person": "品质确认人员",
"quality_time": "品质确认时间",
"device_name": "设备名称",
"device_code": "设备编码",
"node_not_found": "未找到匹配节点",
"query_before_export": "请先查询追溯数据后再导出"
}
}
}
},
"__MENU_TEMP_BEGIN__": "===== 以下为临时菜单翻译,后续统一删除 =====",

View File

@@ -13,6 +13,12 @@ export default {
name: `${pre}index`,
meta: { ...meta, title: '数据中台', root: '/data_middleground' },
component: _import('system/function/module-index')
},
{
path: 'basic_traceability/reverse_direction_traceability',
name: `${pre}basic_traceability-reverse_direction_traceability`,
meta: { ...meta, cache: true, title: '反向追溯' },
component: _import('data-platform/traceability/backward')
}
])('data_middleground-')
}

View File

@@ -0,0 +1,335 @@
<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'))" />
<template v-else>
<div class="trace-toolbar">
<div class="trace-toolbar__group">
<span class="toolbar-label">{{ $t(key('view_type')) }}</span>
<el-radio-group v-model="viewType" size="mini">
<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>
<div class="trace-toolbar__locate">
<span class="toolbar-label">{{ $t(key('position')) }}</span>
<el-input v-model.trim="locator.item_code" size="mini" clearable :placeholder="$t(key('enter_item_code'))" />
<el-button size="mini" type="primary" @click="locateNode('item_code', locator.item_code)">
{{ $t(key('confirm_position')) }}
</el-button>
<el-input v-model.trim="locator.item_batch" size="mini" clearable :placeholder="$t(key('enter_item_batch'))" />
<el-button size="mini" type="primary" @click="locateNode('item_batch', locator.item_batch)">
{{ $t(key('confirm_position')) }}
</el-button>
<el-input
v-model.trim="locator.workingsubclass_code"
size="mini"
clearable
:placeholder="$t(key('enter_work_unit'))"
/>
<el-button size="mini" type="primary" @click="locateNode('workingsubclass_code', locator.workingsubclass_code, true)">
{{ $t(key('confirm_position')) }}
</el-button>
</div>
</div>
<div :class="['trace-tree-wrap', 'trace-tree-wrap--' + viewType]">
<el-tree
ref="tree"
class="trace-tree"
:data="treeData"
node-key="id"
default-expand-all
:expand-on-click-node="false"
>
<template #default="{ data }">
<div :class="['trace-node', { 'is-active': activeNodeId === data.id }]" :data-node-id="data.id" @click="activeNodeId = data.id">
<div class="trace-node__title">
<el-tag size="mini" type="warning" effect="dark">{{ data.data.type_name || '-' }}</el-tag>
<strong>{{ data.text || data.id }}</strong>
</div>
<div class="trace-node__body">
<p>{{ $t(key('item_name')) }}: {{ data.data.item_name || '-' }}</p>
<p>{{ $t(key('item_code')) }}: {{ data.data.item_code || '-' }}</p>
<p>{{ $t(key('item_batch')) }}: {{ data.data.item_batch || '-' }}</p>
<p>{{ $t(key('work_unit')) }}: {{ data.data.workingsubclass_code || '-' }}</p>
<p>{{ $t(key('work_unit_name')) }}: {{ data.data.workingsubclass_name || '-' }}</p>
<p>{{ $t(key('process_code')) }}: {{ data.data.process_code || '-' }}</p>
<p>{{ $t(key('start_time')) }}: {{ data.data.start_time || '-' }}</p>
<p>{{ $t(key('finish_time')) }}: {{ data.data.finish_time || '-' }}</p>
<p>{{ $t(key('inspection_person')) }}: {{ data.data.inspection_person || '-' }}</p>
<p>{{ $t(key('inspection_time')) }}: {{ data.data.inspection_time || '-' }}</p>
<p>{{ $t(key('quality_person')) }}: {{ data.data.quality_person || '-' }}</p>
<p>{{ $t(key('quality_time')) }}: {{ data.data.quality_time || '-' }}</p>
<p>{{ $t(key('device_name')) }}: {{ data.data.device_name || '-' }}</p>
<p>{{ $t(key('device_code')) }}: {{ data.data.device_code || '-' }}</p>
</div>
</div>
</template>
</el-tree>
</div>
</template>
</div>
</d2-container>
</template>
<script>
import { i18nMixin } from '@/composables/useI18n'
import {
exportBackwardTraceabilityTree,
getBackwardTraceabilityData
} from '@/api/data-platform/traceability/backward'
export default {
name: 'data-platform-traceability-backward',
mixins: [i18nMixin('page.data_platform.traceability.backward')],
data () {
return {
loading: false,
form: {
battery_id: this.$route.params.battery_id || ''
},
traceData: {},
viewType: '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
},
treeData () {
if (!this.hasGraph) return []
const nodeMap = new Map()
this.graph.nodes.forEach(node => {
nodeMap.set(String(node.id), {
...node,
id: String(node.id),
data: node.data || {},
children: []
})
})
const childIds = new Set()
const lines = Array.isArray(this.graph.lines) ? this.graph.lines : []
lines.forEach(line => {
const from = nodeMap.get(String(line.from))
const to = nodeMap.get(String(line.to))
if (from && to) {
from.children.push(to)
childIds.add(to.id)
}
})
const rootId = this.graph.rootId ? String(this.graph.rootId) : ''
const root = rootId && nodeMap.get(rootId)
if (root) return [root]
return Array.from(nodeMap.values()).filter(node => !childIds.has(node.id))
}
},
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) : ''
} finally {
this.loading = false
}
},
resetForm () {
this.form.battery_id = ''
this.traceData = {}
this.activeNodeId = ''
this.locator = {
item_code: '',
item_batch: '',
workingsubclass_code: ''
}
},
locateNode (field, value, onlyFinalProduct = false) {
if (!value) return
const nodes = Array.isArray(this.graph.nodes) ? this.graph.nodes : []
const target = 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)
if (this.$refs.tree) {
this.$refs.tree.setCurrentKey(this.activeNodeId)
}
this.$nextTick(() => {
const element = this.$el.querySelector(`[data-node-id="${this.activeNodeId}"]`)
if (element && element.scrollIntoView) {
element.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'center' })
}
})
},
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 {
min-height: 560px;
}
.trace-toolbar {
display: flex;
flex-wrap: wrap;
gap: 10px 18px;
align-items: center;
margin-bottom: 12px;
}
.trace-toolbar__group,
.trace-toolbar__locate {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
}
.trace-toolbar__locate .el-input {
width: 190px;
}
.toolbar-label {
color: #606266;
font-size: 13px;
font-weight: 600;
}
.trace-tree-wrap {
height: calc(100vh - 230px);
min-height: 520px;
overflow: auto;
border: 1px solid #ebeef5;
border-radius: 4px;
background: #f7f9fb;
padding: 12px;
}
.trace-tree-wrap--horizontal {
::v-deep .el-tree-node__children {
display: flex;
flex-wrap: wrap;
gap: 8px 16px;
}
}
.trace-tree {
min-width: max-content;
background: transparent;
}
.trace-node {
width: 300px;
min-height: 300px;
padding: 10px;
margin: 8px 0;
border: 1px solid #dcdfe6;
border-radius: 4px;
background: #fff;
color: #303133;
cursor: pointer;
}
.trace-node.is-active {
border-color: #409eff;
box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.15);
}
.trace-node__title {
display: flex;
gap: 8px;
align-items: center;
margin-bottom: 8px;
}
.trace-node__body p {
margin: 0;
padding: 4px 0;
border-bottom: 1px solid #f0f2f5;
line-height: 1.35;
white-space: normal;
}
</style>