This commit is contained in:
sheng
2026-06-22 17:55:56 +08:00
19 changed files with 1668 additions and 7 deletions

View File

@@ -0,0 +1,31 @@
# 功能测试 - 托盘追溯
> 模块:数据中台 / 基础追溯 / 托盘追溯 (Tray Traceability)
> 路由:`/data_middleground/produce/traceability/tray`
## 测试前置条件
- 测试账号具备访问“托盘追溯”和“电池追溯”的菜单权限。
- 准备至少 1 个存在电池明细和工序时间线的托盘号。
- 后端接口 `tray``traydetail``batteryactive` 可正常访问。
## 测试任务列表
| 序号 | 测试项 | 操作步骤 | 预期结果 |
|---:|---|---|---|
| 1 | 页面入口 | 进入“托盘追溯”页面 | 页面显示托盘号输入框、查询、重置按钮和托盘列表 |
| 2 | 托盘查询 | 输入有效托盘号并查询 | 表格展示托盘、批次、LOT、激活状态、投入电池数和时间信息 |
| 3 | 空数据查询 | 输入无数据托盘号并查询 | 表格显示空态,页面不报错 |
| 4 | 打开电池明细 | 点击某行“电池明细” | 打开全屏抽屉,展示左侧工序时间线和右侧电池明细表 |
| 5 | 明细搜索 | 在抽屉内输入电池条码关键字 | 明细表按电池条码过滤 |
| 6 | 取消激活校验 | 不选择电池,点击取消电池激活 | 页面提示请至少选择一个电池 |
| 7 | 取消激活 | 选择一个或多个电池后点击取消电池激活 | 调用取消激活接口,成功后刷新明细 |
| 8 | 跳转电池追溯 | 点击明细中的电池条码 | 跳转到电池追溯页面并携带 battery_id 查询参数 |
| 9 | 重置功能 | 查询后点击重置 | 托盘号、列表和分页状态清空 |
| 10 | 国际化检查 | 切换中英文语言后重新进入页面 | 页面按钮、表格列和抽屉文案随语言切换显示 |
## 回归关注点
- 电池明细过滤需要排除空电池条码和 0。
- 取消激活接口参数 `batterData` 必须是已选择电池数组的 JSON 字符串。
- 电池条码跳转需保留 `battery_id` 查询参数。

View File

@@ -0,0 +1,23 @@
# 功能测试 - 电池详情报表
> 模块:数据中台 / 生产报表 / 电池详情报表 (Battery Detail Report)
> 路由:`/data_middleground/produce/report/battery-detail`
## 测试任务列表
| 序号 | 测试项 | 操作步骤 | 预期结果 |
|---:|---|---|---|
| 1 | 页面入口 | 进入电池详情报表页面 | 页面显示工艺流程、批次、工序、托盘、时间筛选项 |
| 2 | 工艺选择 | 选择工艺流程 | 批次和工序下拉数据按工艺刷新 |
| 3 | 批次必填校验 | 不选批次点击查询 | 页面提示请选择批次 |
| 4 | 查询报表 | 选择批次和筛选条件后查询 | 表格按动态表头展示电池详情数据 |
| 5 | 分页切换 | 查询出多页数据后切换分页 | 当前页数据刷新,总数正确 |
| 6 | 导出任务 | 选择批次后点击导出并确认 | 创建导出任务成功并跳转任务页面 |
| 7 | 重置功能 | 点击重置 | 筛选项、动态表头、表格和分页清空 |
| 8 | 国际化检查 | 切换中英文语言 | 页面文案随语言切换 |
## 回归关注点
- 查询前必须先获取动态表头。
- 导出任务 action 必须为 `download`
- 后端返回嵌套表头时需要展开为可展示列。

View File

@@ -0,0 +1,31 @@
# 功能测试 - 电池追溯
> 模块:数据中台 / 基础追溯 / 电池追溯 (Battery Traceability)
> 路由:`/data_middleground/produce/traceability/battery`
## 测试前置条件
- 测试账号具备访问“电池追溯”的菜单权限。
- 准备至少 1 个存在工序过程数据的电池条码。
- 准备 1 个已激活电池和 1 个 NG 且未激活电池,用于验证操作按钮。
## 测试任务列表
| 序号 | 测试项 | 操作步骤 | 预期结果 |
|---:|---|---|---|
| 1 | 页面入口 | 进入“电池追溯”页面 | 页面显示电池条码输入框、查询、重置按钮和列表区域 |
| 2 | 电池查询 | 输入有效电池条码并查询 | 表格展示批次、托盘、LOT、激活状态、GOOD/NG、等级、不良信息、当前工序 |
| 3 | URL 参数查询 | 访问路由并携带 `?battery_id=xxx` | 页面自动按该电池条码查询 |
| 4 | 打开电池详情 | 点击某行“电池详情” | 弹出全屏详情,左侧展示工序列表,右侧展示默认工序数据 |
| 5 | 切换工序 | 在详情中点击不同工序 | 右侧工序数据按选中工序刷新 |
| 6 | 工序数据搜索 | 在详情中输入项目名称关键字 | 工序数据表按项目名称过滤 |
| 7 | 取消激活 | 对已激活电池点击取消激活并确认 | 调用取消激活接口,行状态更新为停止 |
| 8 | 复投激活 | 对 NG 且未激活电池点击复投激活并确认 | 调用 Workerman 复投接口,成功后刷新该电池数据 |
| 9 | 重置功能 | 查询后点击重置 | 电池条码、列表和分页状态清空 |
| 10 | 国际化检查 | 切换中英文语言后重新进入页面 | 页面按钮、表格列和弹窗文案随语言切换显示 |
## 回归关注点
- 工序详情接口必须携带当前行的批次、电池条码、托盘、LOT 和工序信息。
- 取消激活接口参数 `batterData` 必须是数组 JSON。
- 复投激活必须发送 `set_battery_rebatch` action。

View File

@@ -0,0 +1,22 @@
# 功能测试 - 设备履历报表
> 模块:数据中台 / 生产报表 / 设备履历报表 (Equipment History Report)
> 路由:`/data_middleground/produce/report/equipment-history`
## 测试任务列表
| 序号 | 测试项 | 操作步骤 | 预期结果 |
|---:|---|---|---|
| 1 | 页面入口 | 进入设备履历报表页面 | 页面显示设备编码、状态、时间范围筛选项和列表 |
| 2 | 设备编码查询 | 输入有效设备编码并查询 | 表格展示设备履历记录 |
| 3 | 状态筛选 | 选择运行/空闲/异常状态查询 | 表格仅展示匹配状态数据 |
| 4 | 时间范围筛选 | 选择开始结束时间后查询 | 表格展示时间范围内履历 |
| 5 | 分页切换 | 查询出多页数据后切换分页 | 当前页数据刷新,总数正确 |
| 6 | 重置功能 | 点击重置 | 筛选项、表格和分页状态清空 |
| 7 | 空数据 | 输入无匹配条件查询 | 显示空态,不出现脚本错误 |
| 8 | 国际化检查 | 切换中英文语言 | 页面文案随语言切换 |
## 回归关注点
- 接口必须调用 `report/device/log`method 为 `get.device.status.log`
- 时间范围需要拆分为 `start_time``end_time`

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
- 已迁移30
- 未迁移49
- 已迁移35
- 未迁移44
| 状态 | 一级模块 | 二级模块 | 三级模块 | 功能说明 | V2 目标路径 |
|:---:|---|---|---|---|---|
@@ -82,11 +82,11 @@
| ✅ | 数据中台 (Data Platform) | 基础追溯 (Traceability) | 反向追溯 (Backward Traceability) | 反向追溯 | `src/views/data-platform/traceability/backward/` |
| ✅ | 数据中台 (Data Platform) | 基础追溯 (Traceability) | 正向追溯 (Forward Traceability) | 正向追溯 | `src/views/data-platform/traceability/forward/` |
| ✅ | 数据中台 (Data Platform) | 基础追溯 (Traceability) | 电池曲线 (Battery Curve) | 电池曲线 | `src/views/data-platform/traceability/battery-curve/` |
| | 数据中台 (Data Platform) | 基础追溯 (Traceability) | 托盘追溯 (Tray Traceability) | | 待确认 |
| | 数据中台 (Data Platform) | 基础追溯 (Traceability) | 电池追溯 (Battery Traceability) | | 待确认 |
| | 数据中台 (Data Platform) | 生产报表 (Production Reports) | 设备履历报表 (Equipment History Report) | | 待确认 |
| | 数据中台 (Data Platform) | 生产报表 (Production Reports) | 电池详情报表 (Battery Detail Report) | | 待确认 |
| | 数据中台 (Data Platform) | 相关性分析 (Correlation Analysis) | 鹰眼 (Hawkeye) | | 待确认 |
| | 数据中台 (Data Platform) | 基础追溯 (Traceability) | 托盘追溯 (Tray Traceability) | 托盘追溯 | `src/views/data-platform/traceability/tray/` |
| | 数据中台 (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) | 鹰眼 | `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

@@ -0,0 +1,23 @@
import { request } from '@/api/_service'
const BASE = 'planning_production/produce/battery_details_report/'
function dataParams (method, data = {}, platform = 'api') {
return { method, platform, ...data }
}
export function getBatteryDetailTitle (data) {
return request({ url: BASE + 'battery_details_title', method: 'post', data: dataParams('production_report_battery_details_title', data) })
}
export function getBatteryDetailList (data) {
return request({ url: BASE + 'battery_details_list', method: 'post', data: dataParams('production_report_battery_details_list', data) })
}
export function getBatteryDetailFlowBatch (data) {
return request({ url: BASE + 'battery_details_flow_batch', method: 'get', params: dataParams('production_report_battery_details_flow_batch', data) })
}
export function createBatteryDetailExportTask (data) {
return request({ url: BASE + 'battery_details_task', method: 'post', data: dataParams('system_exporttask_battery_details_task', data, 'admin') })
}

View File

@@ -0,0 +1,15 @@
import { request } from '@/api/_service'
const BASE = 'report/'
function apiParams (method, data = {}) {
return { method, platform: 'api', ...data }
}
export function getEquipmentHistoryList (data) {
return request({
url: BASE + 'device/log',
method: 'post',
data: apiParams('get.device.status.log', data)
})
}

View File

@@ -0,0 +1,31 @@
import { request } from '@/api/_service'
const BASE = 'planning_production/produce/traceability/'
function apiParams (method, data = {}) {
return { method, platform: 'background', ...data }
}
export function getBatteryTraceList (data) {
return request({
url: BASE + 'battery',
method: 'get',
params: apiParams('planning_production_produce_traceability_battery', data)
})
}
export function getBatteryProcessData (data) {
return request({
url: BASE + 'batteryProcess',
method: 'get',
params: apiParams('planning_production_produce_traceability_batteryProcess', data)
})
}
export function cancelBatteryActive (data) {
return request({
url: BASE + 'batteryactive',
method: 'get',
params: apiParams('planning_production_produce_traceability_batteryactive', data)
})
}

View File

@@ -0,0 +1,31 @@
import { request } from '@/api/_service'
const BASE = 'planning_production/produce/traceability/'
function apiParams (method, data = {}) {
return { method, platform: 'background', ...data }
}
export function getTrayTraceList (data) {
return request({
url: BASE + 'tray',
method: 'get',
params: apiParams('planning_production_produce_traceability_tray', data)
})
}
export function getTrayTraceDetail (data) {
return request({
url: BASE + 'traydetail',
method: 'get',
params: apiParams('planning_production_produce_traceability_traydetail', data)
})
}
export function cancelTraceBatteryActive (data) {
return request({
url: BASE + 'batteryactive',
method: 'get',
params: apiParams('planning_production_produce_traceability_batteryactive', data)
})
}

View File

@@ -1336,6 +1336,151 @@
"current": "Current",
"voltage": "Voltage",
"capacity": "Capacity"
},
"tray": {
"query": "Search",
"reset": "Reset",
"tray_code": "Tray No.",
"tray_code_placeholder": "Enter tray no.",
"data_empty": "No data",
"id": "ID",
"tray": "Tray",
"login_batch": "Login Batch",
"lot": "LOT",
"is_active": "Active",
"active": "Active",
"inactive": "Inactive",
"input_battery_count": "Input Battery Count",
"login_time": "Login Time",
"cancel_active_time": "Cancel Active Time",
"battery_detail": "Battery Detail",
"battery_detail_data": "Battery Detail Data",
"process": "Process",
"start_time": "Start Time",
"end_time": "End Time",
"device_no": "Device No.",
"battery_id": "Battery Barcode",
"search_battery_id": "Search battery barcode",
"cancel_battery_active": "Cancel Battery Active",
"sort": "No.",
"production_batch": "Production Batch",
"model": "Model",
"process_flow_name": "Process Flow Name",
"tray_no": "Tray No.",
"activation_status": "Activation Status",
"category": "Category",
"grade": "Grade",
"please_select_at_least_one_battery": "Select at least one battery",
"cancel_success": "Cancel active successfully"
},
"battery": {
"query": "Search",
"reset": "Reset",
"battery_code": "Battery Barcode",
"enter_battery_code": "Enter battery barcode",
"login_batch": "Login Batch",
"tray_no": "Tray No.",
"lot": "LOT",
"is_active": "Active",
"activated": "Activated",
"stopped": "Stopped",
"good_or_ng": "GOOD/NG",
"grade": "Grade",
"ng_info": "NG Info",
"current_process_code": "Current Process Code",
"battery_detail": "Battery Detail",
"cancel_activation": "Cancel Active",
"reinvestment_activation": "Rework Activation",
"no_data": "No data",
"title": "Battery [{battery_id}] Traceability Detail",
"process_label": "Process",
"process_data": "Process Data",
"item_name": "Item Name",
"content": "Content",
"search_item_name": "Search item name",
"prompt": "Prompt",
"cancel_active_confirm": "Cancel activation for this battery?",
"activation_confirm": "Execute rework activation?",
"cancel_success": "Cancel active successfully",
"activation_success": "Rework activation successfully"
}
},
"production_reports": {
"equipment_history": {
"query": "Search",
"reset": "Reset",
"device_code": "Device Code",
"enter_device_code": "Enter device code",
"device_name": "Device Name",
"status": "Status",
"select_status": "Select status",
"running": "Running",
"idle": "Idle",
"error": "Error",
"status_name": "Status Name",
"time_range": "Time Range",
"start_time": "Start Time",
"end_time": "End Time",
"duration": "Duration",
"remark": "Remark",
"no_data": "No data"
},
"battery_detail": {
"query": "Search",
"reset": "Reset",
"export": "Export",
"flow": "Process Flow",
"select_flow": "Select process flow",
"batch": "Batch",
"select_batch": "Select batch",
"process": "Process",
"select_process": "Select process",
"tray_no": "Tray No.",
"enter_tray_no": "Enter tray no.",
"time": "Completion Time",
"start_date": "Start Time",
"end_date": "End Time",
"no_data": "No data",
"please_select_batch": "Select batch",
"export_confirm": "Create battery detail export task?",
"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

@@ -1336,6 +1336,151 @@
"current": "电流",
"voltage": "电压",
"capacity": "容量"
},
"tray": {
"query": "查询",
"reset": "重置",
"tray_code": "托盘号",
"tray_code_placeholder": "请输入托盘号",
"data_empty": "暂无数据",
"id": "ID",
"tray": "托盘",
"login_batch": "登录批次",
"lot": "LOT",
"is_active": "是否激活",
"active": "激活",
"inactive": "未激活",
"input_battery_count": "投入电池数",
"login_time": "登录时间",
"cancel_active_time": "取消激活时间",
"battery_detail": "电池明细",
"battery_detail_data": "电池明细数据",
"process": "工序",
"start_time": "开始时间",
"end_time": "结束时间",
"device_no": "设备编号",
"battery_id": "电池条码",
"search_battery_id": "搜索电池条码",
"cancel_battery_active": "取消电池激活",
"sort": "序号",
"production_batch": "生产批次",
"model": "型号",
"process_flow_name": "工艺流程名称",
"tray_no": "托盘号",
"activation_status": "激活状态",
"category": "类别",
"grade": "等级",
"please_select_at_least_one_battery": "请至少选择一个电池",
"cancel_success": "取消激活成功"
},
"battery": {
"query": "查询",
"reset": "重置",
"battery_code": "电池条码",
"enter_battery_code": "请输入电池条码",
"login_batch": "登录批次",
"tray_no": "托盘号",
"lot": "LOT",
"is_active": "是否激活",
"activated": "已激活",
"stopped": "已停止",
"good_or_ng": "良品/不良",
"grade": "等级",
"ng_info": "不良信息",
"current_process_code": "当前工序编码",
"battery_detail": "电池详情",
"cancel_activation": "取消激活",
"reinvestment_activation": "复投激活",
"no_data": "暂无数据",
"title": "电池【{battery_id}】追溯详情",
"process_label": "工序",
"process_data": "工序数据",
"item_name": "项目名称",
"content": "内容",
"search_item_name": "搜索项目名称",
"prompt": "提示",
"cancel_active_confirm": "确认取消该电池激活?",
"activation_confirm": "确认执行复投激活?",
"cancel_success": "取消激活成功",
"activation_success": "复投激活成功"
}
},
"production_reports": {
"equipment_history": {
"query": "查询",
"reset": "重置",
"device_code": "设备编码",
"enter_device_code": "请输入设备编码",
"device_name": "设备名称",
"status": "状态",
"select_status": "请选择状态",
"running": "运行",
"idle": "空闲",
"error": "异常",
"status_name": "状态名称",
"time_range": "时间范围",
"start_time": "开始时间",
"end_time": "结束时间",
"duration": "持续时长",
"remark": "备注",
"no_data": "暂无数据"
},
"battery_detail": {
"query": "查询",
"reset": "重置",
"export": "导出",
"flow": "工艺流程",
"select_flow": "请选择工艺流程",
"batch": "批次",
"select_batch": "请选择批次",
"process": "工序",
"select_process": "请选择工序",
"tray_no": "托盘号",
"enter_tray_no": "请输入托盘号",
"time": "完成时间",
"start_date": "开始时间",
"end_date": "结束时间",
"no_data": "暂无数据",
"please_select_batch": "请选择批次",
"export_confirm": "确认创建电池详情报表导出任务?",
"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

@@ -31,6 +31,36 @@ export default {
name: `${pre}traceability-curve`,
meta: { ...meta, cache: true, title: '电池曲线' },
component: _import('data-platform/traceability/battery-curve')
},
{
path: 'produce/traceability/tray',
name: `${pre}traceability-tray`,
meta: { ...meta, cache: true, title: '托盘追溯' },
component: _import('data-platform/traceability/tray')
},
{
path: 'produce/traceability/battery',
name: `${pre}traceability-battery`,
meta: { ...meta, cache: true, title: '电池追溯' },
component: _import('data-platform/traceability/battery')
},
{
path: 'produce/report/equipment-history',
name: `${pre}report-equipment-history`,
meta: { ...meta, cache: true, title: '设备履历报表' },
component: _import('data-platform/production-reports/equipment-history')
},
{
path: 'produce/report/battery-detail',
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>

View File

@@ -0,0 +1,123 @@
<template>
<d2-container>
<template #header>
<div class="search-bar">
<el-form ref="searchForm" :inline="true" :model="search" size="mini">
<el-form-item :label="$t(key('flow'))" prop="flow_idx">
<el-select v-model="search.flow_idx" :placeholder="$t(key('select_flow'))" clearable filterable style="width:180px" @change="onFlowChange">
<el-option v-for="(item, index) in flowOptions" :key="item.id || index" :label="item.name" :value="index" />
</el-select>
</el-form-item>
<el-form-item :label="$t(key('batch'))" prop="batch">
<el-select v-model="search.batch" :placeholder="$t(key('select_batch'))" multiple collapse-tags clearable filterable style="width:220px">
<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="process_id">
<el-select v-model="search.process_id" :placeholder="$t(key('select_process'))" multiple collapse-tags clearable filterable style="width:220px">
<el-option v-for="item in processOptions" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item :label="$t(key('tray_no'))" prop="tray">
<el-input v-model.trim="search.tray" :placeholder="$t(key('enter_tray_no'))" clearable style="width:180px" />
</el-form-item>
<el-form-item :label="$t(key('time'))" prop="time">
<el-date-picker v-model="search.time" type="datetimerange" value-format="yyyy-MM-dd HH:mm:ss" :start-placeholder="$t(key('start_date'))" :end-placeholder="$t(key('end_date'))" style="width:330px" />
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" :disabled="loading" @click="onSearch">{{ $t(key('query')) }}</el-button>
<el-button icon="el-icon-refresh" :disabled="loading" @click="onReset">{{ $t(key('reset')) }}</el-button>
<el-button type="primary" icon="el-icon-download" :disabled="loading" @click="exportTask">{{ $t(key('export')) }}</el-button>
</el-form-item>
</el-form>
</div>
</template>
<el-table v-loading="loading" :data="tableData" size="mini" border height="calc(100vh - 220px)">
<el-table-column type="index" width="55" />
<el-table-column v-for="column in flatColumns" :key="column.prop" :prop="column.prop" :label="column.label" min-width="160" show-overflow-tooltip />
<template #empty><el-empty :description="$t(key('no_data'))" :image-size="80" /></template>
</el-table>
<div class="pager">
<el-pagination background layout="total, sizes, prev, pager, next, jumper" :current-page="pagination.current" :page-size="pagination.size" :total="pagination.total" @current-change="changePage" @size-change="changeSize" />
</div>
</d2-container>
</template>
<script>
import { i18nMixin } from '@/composables/useI18n'
import { createBatteryDetailExportTask, getBatteryDetailFlowBatch, getBatteryDetailList, getBatteryDetailTitle } from '@/api/data-platform/production-reports/battery-detail'
export default {
name: 'data-platform-production-reports-battery-detail',
mixins: [i18nMixin('page.data_platform.production_reports.battery_detail')],
data () {
return { loading: false, search: { flow_idx: '', flow_id: '', batch: [], process_id: [], tray: '', time: [] }, flowOptions: [], batchOptions: [], processOptions: [], title: [], tableData: [], pagination: { current: 1, size: 10, total: 0 } }
},
computed: {
flatColumns () {
const result = []
const walk = list => {
;(Array.isArray(list) ? list : []).forEach(item => {
if (Array.isArray(item.child) && item.child.length) walk(item.child)
else if (item.prop) result.push({ prop: item.prop, label: item.label || item.prop })
})
}
walk(this.title)
if (result.length) return result
const first = this.tableData[0] || {}
return Object.keys(first).slice(0, 30).map(key => ({ prop: key, label: key }))
}
},
mounted () { this.loadOptions() },
methods: {
responseData (res) { return res && res.data !== undefined ? res.data : res },
async loadOptions () {
const res = await getBatteryDetailFlowBatch({ action: 'where_data', batch: 'batch' })
this.flowOptions = this.responseData(res) || []
},
onFlowChange (index) {
const flow = this.flowOptions[index] || {}
this.search.flow_id = flow.id || ''
this.search.batch = []
this.search.process_id = []
this.batchOptions = Array.isArray(flow.batch_manage) ? flow.batch_manage : []
this.processOptions = Array.isArray(flow.process) ? flow.process : []
},
buildParams () {
const time = Array.isArray(this.search.time) ? this.search.time : []
return { ...this.search, start_time: time[0], end_time: time[1], page_no: this.pagination.current, page_size: this.pagination.size }
},
async fetchData () {
if (!this.search.batch || !this.search.batch.length) { this.$message.warning(this.$t(this.key('please_select_batch'))); return }
this.loading = true
try {
const params = this.buildParams()
const titleRes = await getBatteryDetailTitle(params)
this.title = this.responseData(titleRes) || []
const res = await getBatteryDetailList({ ...params, action: 'get_data' })
const data = this.responseData(res)
const list = data && data.data && Array.isArray(data.data.data) ? data.data.data : (Array.isArray(data && data.data) ? data.data : (Array.isArray(data) ? data : []))
this.tableData = list
this.pagination.total = Number(data && data.data && data.data.count) || Number(data && data.count) || list.length
} finally { this.loading = false }
},
onSearch () { this.pagination.current = 1; this.fetchData() },
onReset () { this.search = { flow_idx: '', flow_id: '', batch: [], process_id: [], tray: '', time: [] }; this.batchOptions = []; this.processOptions = []; this.title = []; this.tableData = []; this.pagination.current = 1; this.pagination.total = 0 },
changePage (page) { this.pagination.current = page; this.fetchData() },
changeSize (size) { this.pagination.size = size; this.pagination.current = 1; this.fetchData() },
async exportTask () {
if (!this.search.batch || !this.search.batch.length) { this.$message.warning(this.$t(this.key('please_select_batch'))); return }
await this.$confirm(this.$t(this.key('export_confirm')), this.$t(this.key('prompt')), { type: 'warning' })
await createBatteryDetailExportTask({ ...this.buildParams(), action: 'download' })
this.$message.success(this.$t(this.key('create_download_task_success')))
this.$router.push({ name: 'task' })
}
}
}
</script>
<style lang="scss" scoped>
.search-bar { margin-bottom: -18px; }
.pager { padding-top: 10px; text-align: right; }
</style>

View File

@@ -0,0 +1,108 @@
<template>
<d2-container>
<template #header>
<div class="search-bar">
<el-form ref="searchForm" :inline="true" :model="search" size="mini">
<el-form-item :label="$t(key('device_code'))" prop="device_code">
<el-input v-model.trim="search.device_code" :placeholder="$t(key('enter_device_code'))" clearable style="width:200px" @keyup.enter.native="onSearch" />
</el-form-item>
<el-form-item :label="$t(key('status'))" prop="status">
<el-select v-model="search.status" :placeholder="$t(key('select_status'))" clearable style="width:160px">
<el-option :label="$t(key('running'))" value="running" />
<el-option :label="$t(key('idle'))" value="idle" />
<el-option :label="$t(key('error'))" value="error" />
</el-select>
</el-form-item>
<el-form-item :label="$t(key('time_range'))" prop="time">
<el-date-picker v-model="search.time" type="datetimerange" value-format="yyyy-MM-dd HH:mm:ss" range-separator="-" :start-placeholder="$t(key('start_time'))" :end-placeholder="$t(key('end_time'))" style="width:330px" />
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" :disabled="loading" @click="onSearch">{{ $t(key('query')) }}</el-button>
<el-button icon="el-icon-refresh" :disabled="loading" @click="onReset">{{ $t(key('reset')) }}</el-button>
</el-form-item>
</el-form>
</div>
</template>
<page-table :columns="columns" :data="tableData" :loading="loading" :toolbar-buttons="[]" :row-buttons="[]" :pagination="pagination" :table-attrs="{ size: 'mini', rowKey: rowKey, highlightCurrentRow: true }" auto-height @page-change="onPageChange">
<template #empty><el-empty :description="$t(key('no_data'))" :image-size="80" /></template>
</page-table>
</d2-container>
</template>
<script>
import { useTableColumns } from '@/composables/useTableColumns'
import { i18nMixin } from '@/composables/useI18n'
import PageTable from '@/components/page-table'
import { getEquipmentHistoryList } from '@/api/data-platform/production-reports/equipment-history'
export default {
name: 'data-platform-production-reports-equipment-history',
components: { PageTable },
mixins: [i18nMixin('page.data_platform.production_reports.equipment_history')],
data () {
return {
loading: false,
search: { device_code: '', status: '', time: [] },
tableData: [],
pagination: { current: 1, size: 10, total: 0 }
}
},
computed: {
columns () {
return useTableColumns([
{ prop: 'device_code', label: this.key('device_code'), minWidth: 160, showOverflowTooltip: true },
{ prop: 'device_name', label: this.key('device_name'), minWidth: 160, showOverflowTooltip: true },
{ prop: 'status', label: this.key('status'), minWidth: 120, showOverflowTooltip: true },
{ prop: 'status_name', label: this.key('status_name'), minWidth: 140, showOverflowTooltip: true },
{ prop: 'start_time', label: this.key('start_time'), minWidth: 170, showOverflowTooltip: true },
{ prop: 'end_time', label: this.key('end_time'), minWidth: 170, showOverflowTooltip: true },
{ prop: 'duration', label: this.key('duration'), minWidth: 120, showOverflowTooltip: true },
{ prop: 'remark', label: this.key('remark'), minWidth: 220, showOverflowTooltip: true }
], { selectionWidth: 0, indexWidth: 55 })
}
},
methods: {
rowKey (row) {
return row.id || [row.device_code, row.start_time, row.end_time].filter(Boolean).join('-')
},
responseData (res) {
return res && res.data !== undefined ? res.data : res
},
buildParams () {
const time = Array.isArray(this.search.time) ? this.search.time : []
return { ...this.search, start_time: time[0], end_time: time[1], page_no: this.pagination.current, page_size: this.pagination.size }
},
async fetchData () {
this.loading = true
try {
const res = await getEquipmentHistoryList(this.buildParams())
const data = this.responseData(res)
const list = Array.isArray(data) ? data : (Array.isArray(data && data.data) ? data.data : [])
this.tableData = list
this.pagination.total = Number(data && data.count) || list.length
} finally {
this.loading = false
}
},
onSearch () {
this.pagination.current = 1
this.fetchData()
},
onReset () {
this.search = { device_code: '', status: '', time: [] }
this.pagination.current = 1
this.tableData = []
this.pagination.total = 0
},
onPageChange (page) {
this.pagination = { ...this.pagination, ...page }
this.fetchData()
}
}
}
</script>
<style lang="scss" scoped>
.search-bar { margin-bottom: -18px; }
</style>

View File

@@ -0,0 +1,222 @@
<template>
<d2-container>
<template #header>
<div class="search-bar">
<el-form ref="searchForm" :inline="true" :model="search" size="mini">
<el-form-item :label="$t(key('battery_code'))" prop="battery_id">
<el-input v-model.trim="search.battery_id" :placeholder="$t(key('enter_battery_code'))" clearable style="width:240px" @keyup.enter.native="fetchData" />
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" :disabled="loading" @click="fetchData">{{ $t(key('query')) }}</el-button>
<el-button icon="el-icon-refresh" :disabled="loading" @click="resetSearch">{{ $t(key('reset')) }}</el-button>
</el-form-item>
</el-form>
</div>
</template>
<page-table
:columns="columns"
:data="tableData"
:loading="loading"
:toolbar-buttons="[]"
:row-buttons="rowButtons"
:pagination="pagination"
:table-attrs="{ size: 'mini', rowKey: 'battery_id', highlightCurrentRow: true }"
auto-height
@page-change="onPageChange"
>
<template #col-active="{ row }">
<el-tag :type="Number(row.active) === 1 ? 'success' : 'warning'" size="mini">{{ Number(row.active) === 1 ? $t(key('activated')) : $t(key('stopped')) }}</el-tag>
</template>
<template #col-class="{ row }">
<el-tag v-if="row.class === 'GOOD'" type="success" size="mini">GOOD</el-tag>
<el-tag v-else-if="row.class === 'NG'" type="warning" size="mini">NG</el-tag>
<span v-else>{{ row.class || '-' }}</span>
</template>
<template #empty>
<el-empty :description="$t(key('no_data'))" :image-size="80" />
</template>
</page-table>
<el-dialog :visible.sync="detailVisible" :show-close="false" fullscreen append-to-body>
<div slot="title">
<el-page-header @back="closeDetail" :content="detailTitle" />
</div>
<div class="detail-layout">
<aside class="process-list">
<el-card shadow="never">
<div slot="header">{{ $t(key('process_label')) }}</div>
<el-button
v-for="(item, index) in processList"
:key="index"
class="process-button"
:type="activeProcessIndex === index ? 'primary' : 'default'"
@click="selectProcess(index)"
>
{{ item.process_name || item.process_code || '-' }}
</el-button>
</el-card>
</aside>
<main class="process-detail">
<el-card shadow="never">
<div slot="header">{{ activeProcessName }}{{ $t(key('process_data')) }}</div>
<el-input v-model.trim="detailKeyword" size="mini" clearable :placeholder="$t(key('search_item_name'))" class="detail-search" />
<el-table :data="filteredGridData" border size="mini" height="calc(100vh - 230px)">
<el-table-column type="index" width="60" />
<el-table-column prop="name" :label="$t(key('item_name'))" min-width="180" show-overflow-tooltip />
<el-table-column prop="content" :label="$t(key('content'))" min-width="220" show-overflow-tooltip />
</el-table>
</el-card>
</main>
</div>
</el-dialog>
</d2-container>
</template>
<script>
import { useTableColumns } from '@/composables/useTableColumns'
import { i18nMixin } from '@/composables/useI18n'
import PageTable from '@/components/page-table'
import { sendWorkerman } from '@/api/production-master-data/workerman'
import { cancelBatteryActive, getBatteryProcessData, getBatteryTraceList } from '@/api/data-platform/traceability/battery'
export default {
name: 'data-platform-traceability-battery',
components: { PageTable },
mixins: [i18nMixin('page.data_platform.traceability.battery')],
data () {
return {
loading: false,
search: { battery_id: this.$route.query.battery_id || '' },
tableData: [],
pagination: { current: 1, size: 10, total: 0 },
detailVisible: false,
activeBattery: {},
processList: [],
activeProcessIndex: -1,
gridData: [],
detailKeyword: ''
}
},
computed: {
columns () {
return useTableColumns([
{ prop: 'battery_id', label: this.key('battery_code'), minWidth: 180, showOverflowTooltip: true },
{ prop: 'batch', label: this.key('login_batch'), minWidth: 140, showOverflowTooltip: true },
{ prop: 'tray', label: this.key('tray_no'), minWidth: 130, showOverflowTooltip: true },
{ prop: 'lot', label: this.key('lot'), minWidth: 120, showOverflowTooltip: true },
{ prop: 'active', label: this.key('is_active'), minWidth: 110, slot: 'active' },
{ prop: 'class', label: this.key('good_or_ng'), minWidth: 110, slot: 'class' },
{ prop: 'classname', label: this.key('grade'), minWidth: 120, showOverflowTooltip: true },
{ prop: 'explain', label: this.key('ng_info'), minWidth: 160, showOverflowTooltip: true },
{ prop: 'next_process_code', label: this.key('current_process_code'), minWidth: 160, showOverflowTooltip: true }
], { selectionWidth: 0, indexWidth: 55, operationWidth: 260 })
},
rowButtons () {
return [
{ key: 'detail', label: this.key('battery_detail'), type: 'primary', icon: 'el-icon-document', size: 'mini', onClick: this.openDetail },
{ key: 'cancel', label: this.key('cancel_activation'), type: 'warning', icon: 'el-icon-close', size: 'mini', visible: row => Number(row.active) === 1, onClick: this.cancelActive },
{ key: 'rebatch', label: this.key('reinvestment_activation'), type: 'success', icon: 'el-icon-refresh-right', size: 'mini', visible: row => Number(row.active) === 0 && row.class === 'NG', onClick: this.reinvestmentActivation }
]
},
detailTitle () {
return this.$t(this.key('title'), { battery_id: this.activeBattery.battery_id || '' })
},
activeProcessName () {
const item = this.processList[this.activeProcessIndex] || {}
return item.process_name || item.process_code || '-'
},
filteredGridData () {
const keyword = String(this.detailKeyword || '').toLowerCase()
return this.gridData.filter(item => !keyword || String(item.name || '').toLowerCase().includes(keyword))
}
},
watch: {
'$route.query.battery_id' (value) {
if (value) {
this.search.battery_id = value
this.fetchData()
}
}
},
mounted () {
if (this.search.battery_id) this.fetchData()
},
methods: {
responseData (res) { return res && res.data !== undefined ? res.data : res },
async fetchData () {
this.loading = true
try {
const res = await getBatteryTraceList({ ...this.search, action: 'battery_traceability_search', page_no: this.pagination.current, page_size: this.pagination.size })
const data = this.responseData(res)
this.tableData = Array.isArray(data) ? data : []
this.pagination.total = Number(data && data.count) || this.tableData.length
} finally {
this.loading = false
}
},
resetSearch () {
this.search.battery_id = ''
this.tableData = []
this.pagination.current = 1
this.pagination.total = 0
},
onPageChange (page) {
this.pagination = { ...this.pagination, ...page }
this.fetchData()
},
openDetail (row) {
this.activeBattery = row
this.processList = Array.isArray(row.process) ? row.process : []
this.activeProcessIndex = -1
this.gridData = []
this.detailKeyword = ''
this.detailVisible = true
if (this.processList.length) this.selectProcess(0)
},
closeDetail () {
this.detailVisible = false
},
async selectProcess (index) {
const process = this.processList[index]
if (!process) return
this.activeProcessIndex = index
const res = await getBatteryProcessData({
process_code: process.process_code,
process_id: process.process_id,
batch: this.activeBattery.batch,
battery_id: this.activeBattery.battery_id,
tray: this.activeBattery.tray,
lot: this.activeBattery.lot,
action: 'get_one_battery_data'
})
const data = this.responseData(res)
this.gridData = Array.isArray(data) ? data : []
},
async cancelActive (row) {
await this.$confirm(this.$t(this.key('cancel_active_confirm')), this.$t(this.key('prompt')), { type: 'warning' })
const batterData = [{ batch: row.batch, tray: row.tray, lot: row.lot, battery_id: row.battery_id }]
await cancelBatteryActive({ batterData: JSON.stringify(batterData) })
row.active = 0
this.$message.success(this.$t(this.key('cancel_success')))
},
async reinvestmentActivation (row) {
await this.$confirm(this.$t(this.key('activation_confirm')), this.$t(this.key('prompt')), { type: 'warning' })
const sendData = { action: 'set_battery_rebatch', param: { battery_ids: [row.battery_id] } }
await sendWorkerman({ sendData })
this.$message.success(this.$t(this.key('activation_success')))
this.search.battery_id = row.battery_id
this.fetchData()
}
}
}
</script>
<style lang="scss" scoped>
.search-bar { margin-bottom: -18px; }
.detail-layout { display: grid; grid-template-columns: 260px minmax(0, 1fr); gap: 16px; }
.process-list { max-height: calc(100vh - 150px); overflow: auto; }
.process-button { display: block; width: 100%; margin: 0 0 10px; }
.process-detail { min-width: 0; }
.detail-search { width: 260px; margin-bottom: 12px; }
</style>

View File

@@ -0,0 +1,199 @@
<template>
<d2-container>
<template #header>
<div class="search-bar">
<el-form ref="searchForm" :inline="true" :model="search" size="mini">
<el-form-item :label="$t(key('tray_code'))" prop="tray_id">
<el-input v-model.trim="search.tray_id" :placeholder="$t(key('tray_code_placeholder'))" clearable style="width:220px" @keyup.enter.native="fetchData" />
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" :disabled="loading" @click="fetchData">{{ $t(key('query')) }}</el-button>
<el-button icon="el-icon-refresh" :disabled="loading" @click="resetSearch">{{ $t(key('reset')) }}</el-button>
</el-form-item>
</el-form>
</div>
</template>
<page-table
:columns="columns"
:data="tableData"
:loading="loading"
:toolbar-buttons="[]"
:row-buttons="rowButtons"
:pagination="pagination"
:table-attrs="{ size: 'mini', rowKey: 'id', highlightCurrentRow: true }"
auto-height
@page-change="onPageChange"
>
<template #col-active="{ row }">
<el-tag :type="Number(row.active) === 1 ? 'success' : 'info'" size="mini">
{{ Number(row.active) === 1 ? $t(key('active')) : $t(key('inactive')) }}
</el-tag>
</template>
<template #empty>
<el-empty :description="$t(key('data_empty'))" :image-size="80" />
</template>
</page-table>
<el-drawer :visible.sync="detailVisible" :with-header="false" size="100%" append-to-body>
<div class="drawer-header">
<el-page-header @back="detailVisible = false" :content="$t(key('battery_detail_data'))" />
</div>
<div class="detail-layout">
<aside v-if="timeLine.length" class="detail-timeline">
<el-timeline>
<el-timeline-item v-for="(item, index) in timeLine" :key="index">
<p>{{ $t(key('process')) }}: {{ item.process_name || '-' }}</p>
<el-card shadow="never">
<p>{{ $t(key('start_time')) }}: {{ item.beginTime || '-' }}</p>
<p>{{ $t(key('end_time')) }}: {{ item.endTime || '-' }}</p>
<p>{{ $t(key('device_no')) }}: {{ item.device_code || '-' }}</p>
</el-card>
</el-timeline-item>
</el-timeline>
</aside>
<main class="detail-main">
<el-form :inline="true" :model="detailSearch" size="mini">
<el-form-item :label="$t(key('battery_id'))">
<el-input v-model.trim="detailSearch.battery_id" :placeholder="$t(key('search_battery_id'))" clearable style="width:220px" />
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-close" @click="cancelBatteryActive">{{ $t(key('cancel_battery_active')) }}</el-button>
</el-form-item>
</el-form>
<el-table :data="filteredDetails" border size="mini" height="calc(100vh - 170px)" @selection-change="selection = $event">
<el-table-column type="selection" width="48" />
<el-table-column prop="id" :label="$t(key('sort'))" width="70" />
<el-table-column prop="battery_id" :label="$t(key('battery_id'))" min-width="180" show-overflow-tooltip>
<template slot-scope="scope">
<el-link type="primary" @click="goBatteryTrace(scope.row.battery_id)">{{ scope.row.battery_id }}</el-link>
</template>
</el-table-column>
<el-table-column prop="batch" :label="$t(key('production_batch'))" min-width="140" show-overflow-tooltip />
<el-table-column prop="model" :label="$t(key('model'))" min-width="120" show-overflow-tooltip />
<el-table-column prop="flow_name" :label="$t(key('process_flow_name'))" min-width="160" show-overflow-tooltip />
<el-table-column prop="tray" :label="$t(key('tray_no'))" min-width="130" show-overflow-tooltip />
<el-table-column prop="lot" :label="$t(key('lot'))" min-width="120" show-overflow-tooltip />
<el-table-column prop="active" :label="$t(key('activation_status'))" min-width="120" />
<el-table-column prop="class" :label="$t(key('category'))" min-width="120" />
<el-table-column prop="classname" :label="$t(key('grade'))" min-width="120" />
</el-table>
</main>
</div>
</el-drawer>
</d2-container>
</template>
<script>
import { useTableColumns } from '@/composables/useTableColumns'
import { i18nMixin } from '@/composables/useI18n'
import PageTable from '@/components/page-table'
import { cancelTraceBatteryActive, getTrayTraceDetail, getTrayTraceList } from '@/api/data-platform/traceability/tray'
export default {
name: 'data-platform-traceability-tray',
components: { PageTable },
mixins: [i18nMixin('page.data_platform.traceability.tray')],
data () {
return {
loading: false,
detailLoading: false,
search: { tray_id: this.$route.query.tray || '' },
tableData: [],
pagination: { current: 1, size: 10, total: 0 },
detailVisible: false,
activeTrayId: '',
detailData: [],
timeLine: [],
detailSearch: { battery_id: '' },
selection: []
}
},
computed: {
columns () {
return useTableColumns([
{ prop: 'id', label: this.key('id'), width: 90, sortable: 'custom' },
{ prop: 'tray', label: this.key('tray'), minWidth: 140, showOverflowTooltip: true },
{ prop: 'batch', label: this.key('login_batch'), minWidth: 140, showOverflowTooltip: true },
{ prop: 'lot', label: this.key('lot'), minWidth: 120, showOverflowTooltip: true },
{ prop: 'active', label: this.key('is_active'), minWidth: 110, slot: 'active' },
{ prop: 'input_battery_count', label: this.key('input_battery_count'), minWidth: 140 },
{ prop: 'create_time', label: this.key('login_time'), minWidth: 170, showOverflowTooltip: true },
{ prop: 'cancel_active_time', label: this.key('cancel_active_time'), minWidth: 170, showOverflowTooltip: true }
], { selectionWidth: 0, indexWidth: 55, operationWidth: 130 })
},
rowButtons () {
return [{ key: 'detail', label: this.key('battery_detail'), type: 'primary', icon: 'el-icon-document', size: 'mini', onClick: this.openDetail }]
},
filteredDetails () {
const keyword = String(this.detailSearch.battery_id || '').toLowerCase()
return this.detailData.filter(item => !keyword || String(item.battery_id || '').toLowerCase().includes(keyword))
}
},
mounted () {
if (this.search.tray_id) this.fetchData()
},
methods: {
responseData (res) { return res && res.data !== undefined ? res.data : res },
async fetchData () {
this.loading = true
try {
const res = await getTrayTraceList({ ...this.search, page_no: this.pagination.current, page_size: this.pagination.size })
const data = this.responseData(res)
this.tableData = Array.isArray(data) ? data : []
this.pagination.total = Number(data && data.count) || this.tableData.length
} finally {
this.loading = false
}
},
resetSearch () {
this.search.tray_id = ''
this.tableData = []
this.pagination.current = 1
this.pagination.total = 0
},
onPageChange (page) {
this.pagination = { ...this.pagination, ...page }
this.fetchData()
},
async openDetail (row) {
this.activeTrayId = row.id
this.detailVisible = true
await this.loadDetail()
},
async loadDetail () {
if (!this.activeTrayId) return
this.detailLoading = true
try {
const res = await getTrayTraceDetail({ tray_id: this.activeTrayId })
const data = this.responseData(res) || {}
this.detailData = Array.isArray(data.data) ? data.data.filter(item => item.battery_id !== 0 && item.battery_id !== '') : []
this.timeLine = Array.isArray(data.date_log) ? data.date_log : []
} finally {
this.detailLoading = false
}
},
async cancelBatteryActive () {
if (!this.selection.length) {
this.$message.error(this.$t(this.key('please_select_at_least_one_battery')))
return
}
await cancelTraceBatteryActive({ batterData: JSON.stringify(this.selection) })
this.$message.success(this.$t(this.key('cancel_success')))
this.loadDetail()
},
goBatteryTrace (batteryId) {
this.detailVisible = false
this.$router.push({ path: '/data_middleground/produce/traceability/battery', query: { battery_id: batteryId } })
}
}
}
</script>
<style lang="scss" scoped>
.search-bar { margin-bottom: -18px; }
.drawer-header { padding: 20px 24px; border-bottom: 1px solid #ebeef5; }
.detail-layout { display: grid; grid-template-columns: 320px minmax(0, 1fr); height: calc(100vh - 73px); }
.detail-timeline { overflow: auto; padding: 18px; background: #f5f7fa; border-right: 1px solid #ebeef5; }
.detail-main { min-width: 0; padding: 16px; }
</style>