迁移鹰眼模块
Some checks failed
Release pipeline / publish (push) Has been cancelled
Release pipeline / Always run job (push) Has been cancelled

This commit is contained in:
sheng
2026-06-22 17:51:29 +08:00
parent 973c780bf6
commit c07f95a45e
7 changed files with 563 additions and 3 deletions

View File

@@ -0,0 +1,36 @@
# 鹰眼功能测试任务列表
## 基础入口
- [ ] 进入 `数据中台 / 相关性分析 / 鹰眼`,页面正常加载,无控制台报错。
- [ ] 左侧分析条件区域显示生产批次、工序、NG 码三个筛选项和分析按钮。
- [ ] 页面右侧显示分析详情区域,未查询时为空状态。
## 查询条件
- [ ] 选择生产批次后,工序下拉框自动加载该批次下的不良工序列。
- [ ] 选择工序后NG 码下拉框自动切换为该工序对应的不良代码。
- [ ] 点击重置后批次、工序、NG 码、相关性结果和图表全部清空。
## 相关性分析
- [ ] 未选择生产批次时点击分析,系统提示需要选择批次。
- [ ] 未选择工序时点击分析,系统提示需要选择工序。
- [ ] 选择有效批次、工序和 NG 码后点击分析Pearson 相关性散点图正常渲染。
- [ ] PCC 表格显示工序参数、样本量、相关系数、P 值和相关性判断。
- [ ] 卡方表格显示工序参数、样本量、卡方值、P 值和相关性判断。
- [ ] 有不能参与分析的数据列时,顶部折叠区域展示对应参数名称。
## 图表与交互
- [ ] 散点图鼠标悬停时显示参数名称、相关系数和 P 值。
- [ ] P 值大于 0.05 的记录以蓝色相关状态显示。
- [ ] P 值小于或等于 0.05 的记录以红色不相关状态显示。
- [ ] 已选择 NG 码时点击表格中的相关性文字,弹出分析报告弹窗。
- [ ] 分析报告弹窗内按分类展示折线分布图,关闭后再次打开能正常刷新。
## 兼容性
- [ ] 页面在 1366px 宽度下表格和图表不重叠。
- [ ] 浏览器窗口缩放后图表能自动适配。
- [ ] 切换不同批次再次分析,旧批次结果不会残留。

View File

@@ -3,8 +3,8 @@
> 根据 `后台Webman界面截图对照表.md` 生成。状态以当前 V2 项目中已落地的页面目录为准。
- 总功能数79
- 已迁移34
- 未迁移45
- 已迁移35
- 未迁移44
| 状态 | 一级模块 | 二级模块 | 三级模块 | 功能说明 | V2 目标路径 |
|:---:|---|---|---|---|---|
@@ -86,7 +86,7 @@
| ✅ | 数据中台 (Data Platform) | 基础追溯 (Traceability) | 电池追溯 (Battery Traceability) | 电池追溯 | `src/views/data-platform/traceability/battery/` |
| ✅ | 数据中台 (Data Platform) | 生产报表 (Production Reports) | 设备履历报表 (Equipment History Report) | 设备履历报表 | `src/views/data-platform/production-reports/equipment-history/` |
| ✅ | 数据中台 (Data Platform) | 生产报表 (Production Reports) | 电池详情报表 (Battery Detail Report) | 电池详情报表 | `src/views/data-platform/production-reports/battery-detail/` |
| | 数据中台 (Data Platform) | 相关性分析 (Correlation Analysis) | 鹰眼 (Hawkeye) | | 待确认 |
| | 数据中台 (Data Platform) | 相关性分析 (Correlation Analysis) | 鹰眼 (Hawkeye) | 鹰眼 | `src/views/data-platform/correlation-analysis/hawkeye/` |
## 状态说明

View File

@@ -0,0 +1,43 @@
import { request } from '@/api/_service'
const BASE = 'data_middleground/eagle_eyes/'
function apiParams (method, data = {}) {
return {
method,
platform: 'admin',
...data
}
}
export function getNGWorkstationBatch (data) {
return request({
url: BASE + 'getNGWorkstationBatch',
method: 'get',
params: apiParams('data_middleground_eagle_eyes_getNGWorkstationBatch', data)
})
}
export function getBatchResultParam (data) {
return request({
url: BASE + 'getBatchResultParam',
method: 'get',
params: apiParams('data_middleground_eagle_eyes_getBatchResultParam', data)
})
}
export function analyzeHawkeyeCorrelation (data) {
return request({
url: BASE + 'saveCsvFile',
method: 'get',
params: apiParams('data_middleground_eagle_eyes_saveCsvFile', data)
})
}
export function getDataClassificationByNGCode (data) {
return request({
url: BASE + 'getDataClassificationByNGCode',
method: 'get',
params: apiParams('data_middleground_eagle_eyes_get_data_classification_by_ng_code', data)
})
}

View File

@@ -1378,6 +1378,42 @@
"prompt": "Prompt",
"create_download_task_success": "Download task created successfully"
}
},
"correlation_analysis": {
"hawkeye": {
"analysis_condition": "Analysis Conditions",
"analyze": "Analyze",
"reset": "Reset",
"production_batch": "Production Batch",
"select_production_batch": "Select production batch",
"process": "Process",
"select_ng_column": "Select NG column",
"ng_code": "NG Code",
"select_ng_code": "Select NG code",
"analysis_detail": "Analysis Detail",
"correlated": "Correlated",
"not_correlated": "Not Correlated",
"correlated_short": "Related",
"not_correlated_short": "Not Related",
"no_analysis_data_hint": "Columns skipped because values are all identical or all different",
"process_param": "Process Parameter",
"sample_size": "Sample Size",
"correlation_coeff": "Correlation Coeff.",
"chi_square_value": "Chi-square Value",
"p_value": "P Value",
"correlation": "Correlation",
"p_value_hint": "P value greater than 0.05 is considered correlated",
"p_value_hint2": "P value greater than 0.05 is considered correlated",
"scientific_notation_hint": "Value may be shown in scientific notation",
"tooltip_dependent_var": "Dependent Variable",
"tooltip_corr_coeff": "Correlation Coeff.",
"tooltip_p_value": "P Value",
"analysis_report": "Analysis Report",
"total": "Total",
"no_data": "No analysis data",
"please_select_batch": "Select production batch",
"please_select_process": "Select process"
}
}
}
},

View File

@@ -1378,6 +1378,42 @@
"prompt": "提示",
"create_download_task_success": "创建下载任务成功"
}
},
"correlation_analysis": {
"hawkeye": {
"analysis_condition": "分析条件",
"analyze": "分析",
"reset": "重置",
"production_batch": "生产批次",
"select_production_batch": "请选择生产批次",
"process": "工序",
"select_ng_column": "请选择NG列",
"ng_code": "NG码",
"select_ng_code": "请选择NG码",
"analysis_detail": "分析详情",
"correlated": "相关",
"not_correlated": "不相关",
"correlated_short": "相关",
"not_correlated_short": "不相关",
"no_analysis_data_hint": "因列中数据完全相同或完全不相同,而不进行分析的数据列",
"process_param": "工序参数",
"sample_size": "样本量",
"correlation_coeff": "相关系数",
"chi_square_value": "卡方值",
"p_value": "P值",
"correlation": "相关性",
"p_value_hint": "P值大于0.05时判定为相关",
"p_value_hint2": "P值大于0.05时判定为相关",
"scientific_notation_hint": "数值可能以科学计数法显示",
"tooltip_dependent_var": "因变量",
"tooltip_corr_coeff": "相关系数",
"tooltip_p_value": "P值",
"analysis_report": "分析报告",
"total": "总数",
"no_data": "暂无分析数据",
"please_select_batch": "请选择生产批次",
"please_select_process": "请选择工序"
}
}
}
},

View File

@@ -55,6 +55,12 @@ export default {
name: `${pre}report-battery-detail`,
meta: { ...meta, cache: true, title: '电池详情报表' },
component: _import('data-platform/production-reports/battery-detail')
},
{
path: 'pancake/eagle_eyes',
name: `${pre}formation-data-record`,
meta: { ...meta, cache: true, title: '鹰眼' },
component: _import('data-platform/correlation-analysis/hawkeye')
}
])('data_middleground-')
}

View File

@@ -0,0 +1,403 @@
<template>
<d2-container class="hawkeye-page">
<div class="hawkeye-layout">
<aside class="condition-panel">
<div class="panel-header">
<span>{{ $t(key('analysis_condition')) }}</span>
<el-button type="primary" size="mini" icon="el-icon-data-analysis" :loading="loading" @click="onAnalyze">{{ $t(key('analyze')) }}</el-button>
</div>
<el-form ref="searchForm" :model="search" label-position="top" size="mini" class="condition-form">
<el-form-item :label="$t(key('production_batch'))" prop="table">
<el-select v-model="search.table" filterable clearable :placeholder="$t(key('select_production_batch'))" @change="onBatchChange">
<el-option v-for="item in batchOptions" :key="item.id || item.batch" :label="item.batch" :value="item.batch" />
</el-select>
</el-form-item>
<el-form-item :label="$t(key('process'))" prop="ng_colnum">
<el-select v-model="search.ng_colnum" filterable clearable :placeholder="$t(key('select_ng_column'))" value-key="process_code" @change="onProcessChange">
<el-option v-for="(item, index) in processOptions" :key="index" :label="formatProcessOption(item)" :value="item" />
</el-select>
</el-form-item>
<el-form-item :label="$t(key('ng_code'))" prop="ng_code">
<el-select v-model="search.ng_code" filterable clearable :placeholder="$t(key('select_ng_code'))">
<el-option v-for="(item, index) in ngCodeOptions" :key="index" :label="item.explain || item.name || item.code || item" :value="item.explain || item.code || item" />
</el-select>
</el-form-item>
<el-button icon="el-icon-refresh" size="mini" :disabled="loading" @click="onReset">{{ $t(key('reset')) }}</el-button>
</el-form>
</aside>
<main class="result-panel">
<div class="panel-header">
<span>{{ $t(key('analysis_detail')) }}</span>
<div class="legend">
<span><i class="legend-dot is-related" />{{ $t(key('correlated')) }}</span>
<span><i class="legend-dot is-unrelated" />{{ $t(key('not_correlated')) }}</span>
</div>
</div>
<el-collapse v-if="noCorrColumns.length" class="excluded-panel">
<el-collapse-item :title="$t(key('no_analysis_data_hint'))" name="excluded">
<el-tag v-for="item in noCorrColumns" :key="item" size="mini" type="warning">{{ getParamName(item) }}</el-tag>
</el-collapse-item>
</el-collapse>
<div v-loading="loading" class="analysis-content">
<el-empty v-if="!hasAnalysis" :description="$t(key('no_data'))" :image-size="90" />
<template v-else>
<section class="chart-row">
<div ref="pccChart" class="chart-box" />
<el-table :data="tableData.pcc" size="mini" border height="100%">
<el-table-column prop="process" :label="$t(key('process_param'))" min-width="160" show-overflow-tooltip />
<el-table-column prop="sample_size" :label="$t(key('sample_size'))" width="90" />
<el-table-column prop="corr_data" :label="$t(key('correlation_coeff'))" width="120" />
<el-table-column prop="pv" :label="$t(key('p_value'))" width="110" />
<el-table-column :label="$t(key('correlation'))" width="110">
<template slot="header">
{{ $t(key('correlation')) }}
<el-tooltip :content="$t(key('p_value_hint'))" placement="top">
<i class="el-icon-question" />
</el-tooltip>
</template>
<template slot-scope="scope">
<el-button type="text" :class="relationClass(scope.row.pv)" @click="showDistribution(scope.row)">{{ relationText(scope.row.pv) }}</el-button>
</template>
</el-table-column>
</el-table>
</section>
<section v-if="tableData.chi.length" class="table-section">
<el-table :data="tableData.chi" size="mini" border height="100%">
<el-table-column prop="process" :label="$t(key('process_param'))" min-width="220" show-overflow-tooltip />
<el-table-column prop="sample_size" :label="$t(key('sample_size'))" width="120" />
<el-table-column prop="corr_data" :label="$t(key('chi_square_value'))" width="150" />
<el-table-column prop="pv" width="140">
<template slot="header">
{{ $t(key('p_value')) }}
<el-tooltip :content="$t(key('scientific_notation_hint'))" placement="top">
<i class="el-icon-question" />
</el-tooltip>
</template>
</el-table-column>
<el-table-column width="120">
<template slot="header">
{{ $t(key('correlation')) }}
<el-tooltip :content="$t(key('p_value_hint2'))" placement="top">
<i class="el-icon-question" />
</el-tooltip>
</template>
<template slot-scope="scope">
<el-button type="text" :class="relationClass(scope.row.pv)" @click="showDistribution(scope.row)">{{ relationText(scope.row.pv) }}</el-button>
</template>
</el-table-column>
</el-table>
</section>
</template>
</div>
</main>
</div>
<el-dialog :title="$t(key('analysis_report'))" :visible.sync="distributionDialog.visible" width="80%" @closed="closeDistribution">
<div ref="distributionChart" class="distribution-chart" />
</el-dialog>
</d2-container>
</template>
<script>
import * as echarts from 'echarts'
import { i18nMixin } from '@/composables/useI18n'
import { getBatchAll } from '@/api/planning-production/batch-list'
import { analyzeHawkeyeCorrelation, getBatchResultParam, getDataClassificationByNGCode, getNGWorkstationBatch } from '@/api/data-platform/correlation-analysis/hawkeye'
export default {
name: 'data-platform-correlation-analysis-hawkeye',
mixins: [i18nMixin('page.data_platform.correlation_analysis.hawkeye')],
data () {
return {
loading: false,
search: { table: '', ng_colnum: '', ng_code: undefined },
batchOptions: [],
processOptions: [],
ngCodeOptions: [],
processParamNames: {},
noCorrColumns: [],
tableData: { pcc: [], chi: [] },
pccChart: null,
distributionChart: null,
distributionDialog: { visible: false, data: {} }
}
},
computed: {
hasAnalysis () {
return this.tableData.pcc.length > 0 || this.tableData.chi.length > 0
}
},
mounted () {
this.loadBatchOptions()
window.addEventListener('resize', this.resizeCharts)
},
beforeDestroy () {
window.removeEventListener('resize', this.resizeCharts)
if (this.pccChart) this.pccChart.dispose()
if (this.distributionChart) this.distributionChart.dispose()
},
methods: {
responseData (res) { return res && res.data !== undefined ? res.data : res },
async loadBatchOptions () {
const res = await getBatchAll({})
this.batchOptions = this.responseData(res) || []
},
async onBatchChange (batch) {
this.search.ng_colnum = ''
this.search.ng_code = undefined
this.processOptions = []
this.ngCodeOptions = []
this.processParamNames = {}
if (!batch) return
const [processRes, paramRes] = await Promise.all([
getNGWorkstationBatch({ batch }),
getBatchResultParam({ batch })
])
this.processOptions = this.responseData(processRes) || []
const paramData = this.responseData(paramRes) || {}
this.processParamNames = paramData.result_param_names || {}
},
onProcessChange (value) {
this.search.ng_code = undefined
this.ngCodeOptions = Array.isArray(value) ? value : []
},
formatProcessOption (item) {
const first = Array.isArray(item) ? item[0] : item
return (first && (first.name || first.process_name || first.process_code)) || ''
},
buildAnalyzeParams () {
const ngColumn = Array.isArray(this.search.ng_colnum) ? this.search.ng_colnum[0] : this.search.ng_colnum
return {
...this.search,
corr_code: [],
ng_colnum: ngColumn && ngColumn.process_code ? ngColumn.process_code : ngColumn
}
},
async onAnalyze () {
if (!this.search.table) { this.$message.warning(this.$t(this.key('please_select_batch'))); return }
if (!this.search.ng_colnum) { this.$message.warning(this.$t(this.key('please_select_process'))); return }
this.loading = true
try {
const res = await analyzeHawkeyeCorrelation(this.buildAnalyzeParams())
this.applyAnalysisData(this.responseData(res) || {})
} finally {
this.loading = false
}
},
applyAnalysisData (data) {
const x2 = Array.isArray(data.X2) ? data.X2 : []
const pcc = Array.isArray(data.PCC) ? data.PCC : []
this.noCorrColumns = Array.isArray(data.no_corr_colnum) ? data.no_corr_colnum : []
this.tableData.pcc = pcc.map(item => ({
process: this.getParamName(item.independent_variable_name),
dependent_variable: item.independent_variable_name,
sample_size: this.formatNumber(item.value && item.value[3]),
corr_data: this.formatNumber(item.value && item.value[0]),
pv: item.value && item.value[2]
}))
this.tableData.chi = x2.map(item => ({
process: this.getParamName(item.independent_variable_name),
dependent_variable: item.independent_variable_name,
sample_size: this.formatNumber(item.value && item.value[0]),
corr_data: this.formatNumber(item.value && item.value[1]),
pv: item.value && item.value[2]
}))
this.$nextTick(() => this.renderPccChart(pcc))
},
renderPccChart (pcc) {
const chartDom = this.$refs.pccChart
if (!chartDom) return
if (!this.pccChart) this.pccChart = echarts.init(chartDom)
const points = pcc.map(item => ({
name: this.getParamName(item.independent_variable_name),
value: [Number(item.value && item.value[0]) || 0, Number(item.value && item.value[2]) || 0],
sampleSize: item.value && item.value[3]
}))
this.pccChart.setOption({
title: { text: this.$t(this.key('tooltip_corr_coeff')), left: 'center', textStyle: { fontSize: 14 } },
grid: { left: 52, right: 28, top: 58, bottom: 42 },
tooltip: {
trigger: 'item',
formatter: params => {
const item = params.data
return `${this.$t(this.key('tooltip_dependent_var'))}: ${item.name}<br>${this.$t(this.key('tooltip_corr_coeff'))}: ${this.formatNumber(item.value[0])}<br>${this.$t(this.key('tooltip_p_value'))}: ${this.formatNumber(item.value[1])}`
}
},
xAxis: { type: 'value', name: this.$t(this.key('tooltip_corr_coeff')) },
yAxis: { type: 'value', name: this.$t(this.key('tooltip_p_value')) },
series: [{
type: 'scatter',
data: points,
symbolSize: 9,
itemStyle: { color: params => (Number(params.data.value[1]) > 0.05 ? '#409EFF' : '#F56C6C') }
}]
}, true)
this.pccChart.resize()
},
async showDistribution (row) {
if (!this.search.ng_code || !row.dependent_variable) return
const res = await getDataClassificationByNGCode({ ng_code: this.search.ng_code, dependent_variable: row.dependent_variable })
this.distributionDialog.data = this.responseData(res) || {}
this.distributionDialog.visible = true
this.$nextTick(this.renderDistributionChart)
},
renderDistributionChart () {
const chartDom = this.$refs.distributionChart
if (!chartDom) return
if (!this.distributionChart) this.distributionChart = echarts.init(chartDom)
const raw = Array.isArray(this.distributionDialog.data.col3) ? this.distributionDialog.data.col3 : []
const grouped = this.groupDistribution(raw)
const xAxis = grouped.values
this.distributionChart.setOption({
legend: { data: grouped.classNames, left: 10 },
tooltip: { trigger: 'axis' },
grid: { left: 48, right: 28, top: 48, bottom: 42 },
xAxis: { type: 'category', data: xAxis },
yAxis: { type: 'value', name: this.$t(this.key('total')) },
series: grouped.classNames.map(name => ({ name, type: 'line', smooth: true, symbol: 'none', data: xAxis.map(value => grouped.map[name][value] || 0) }))
}, true)
this.distributionChart.resize()
},
groupDistribution (raw) {
const classNames = Array.from(new Set(raw.map(item => item.classname))).filter(Boolean)
const values = Array.from(new Set(raw.map(item => String(item.value)))).sort((a, b) => Number(a) - Number(b))
const map = {}
classNames.forEach(name => { map[name] = {}; values.forEach(value => { map[name][value] = 0 }) })
raw.forEach(item => {
const className = item.classname
const value = String(item.value)
if (map[className] && Object.prototype.hasOwnProperty.call(map[className], value)) map[className][value] += 1
})
return { classNames, values, map }
},
getParamName (code) {
return this.processParamNames[code] || code || ''
},
relationClass (pv) {
return Number(pv) > 0.05 ? 'relation-related' : 'relation-unrelated'
},
relationText (pv) {
return Number(pv) > 0.05 ? this.$t(this.key('correlated_short')) : this.$t(this.key('not_correlated_short'))
},
formatNumber (value) {
if (value === undefined || value === null || value === '') return ''
const number = Number(value)
if (!Number.isFinite(number)) return value
return Math.abs(number) < 0.001 && number !== 0 ? number.toExponential(2) : Number(number.toFixed(4))
},
resizeCharts () {
if (this.pccChart) this.pccChart.resize()
if (this.distributionChart) this.distributionChart.resize()
},
closeDistribution () {
this.distributionDialog.data = {}
},
onReset () {
this.search = { table: '', ng_colnum: '', ng_code: undefined }
this.processOptions = []
this.ngCodeOptions = []
this.noCorrColumns = []
this.tableData = { pcc: [], chi: [] }
if (this.pccChart) this.pccChart.clear()
}
}
}
</script>
<style lang="scss" scoped>
.hawkeye-page {
::v-deep .d2-container-full__body { padding: 0; }
}
.hawkeye-layout {
display: grid;
grid-template-columns: 300px minmax(0, 1fr);
height: calc(100vh - 132px);
min-height: 640px;
background: #fff;
}
.condition-panel {
border-right: 1px solid #e5e9f2;
overflow: auto;
}
.panel-header {
display: flex;
align-items: center;
justify-content: space-between;
min-height: 44px;
padding: 8px 14px;
background: #f5f7fa;
color: #303133;
font-weight: 600;
}
.condition-form {
padding: 14px;
::v-deep .el-select { width: 100%; }
}
.result-panel {
min-width: 0;
display: flex;
flex-direction: column;
}
.legend {
display: flex;
gap: 14px;
align-items: center;
font-size: 12px;
font-weight: 400;
}
.legend-dot {
display: inline-block;
width: 10px;
height: 10px;
margin-right: 5px;
border-radius: 50%;
}
.is-related { background: #409eff; }
.is-unrelated { background: #f56c6c; }
.excluded-panel {
padding: 0 12px;
::v-deep .el-tag { margin: 0 6px 6px 0; }
}
.analysis-content {
flex: 1;
min-height: 0;
overflow: auto;
padding: 14px;
}
.chart-row {
display: grid;
grid-template-columns: minmax(360px, 1fr) minmax(420px, 46%);
gap: 14px;
height: 360px;
min-width: 860px;
}
.chart-box {
height: 100%;
border: 1px solid #ebeef5;
}
.table-section {
height: 300px;
min-width: 860px;
margin-top: 14px;
}
.relation-related { color: #409eff; }
.relation-unrelated { color: #f56c6c; }
.distribution-chart {
height: 560px;
width: 100%;
}
@media (max-width: 1100px) {
.hawkeye-layout {
grid-template-columns: 1fr;
height: auto;
}
.condition-panel {
border-right: 0;
border-bottom: 1px solid #e5e9f2;
}
}
</style>