Files
MES/app/helpers/field_helper.php
2026-08-08 18:28:49 +08:00

976 lines
52 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* 工位字段配置辅助函数
* 用于后台配置和前端动态渲染
*/
/**
* 获取各工位类型的默认字段配置
*/
function getDefaultFieldsConfig($stationType) {
$configs = [
'inbound' => [
['name'=>'product_type','label'=>'类型','type'=>'select','source'=>'type_list','required'=>true,'enabled'=>true,'placeholder'=>'请选择类型','col_width'=>2,'is_scan'=>false,'show_in_table'=>true,'table_label'=>'类型'],
['name'=>'product_model','label'=>'产品型号','type'=>'select','source'=>'product_models','required'=>true,'enabled'=>true,'placeholder'=>'请选择产品型号','col_width'=>2,'is_scan'=>false,'show_in_table'=>false,'table_label'=>'产品型号'],
['name'=>'serial_no','label'=>'产品序列号','type'=>'text','required'=>true,'enabled'=>true,'placeholder'=>'扫描/输入后按回车','col_width'=>3,'is_scan'=>true,'show_in_table'=>true,'table_label'=>'产品序列号'],
],
'pcb_test' => [
['name'=>'product_type','label'=>'产品类型','type'=>'hidden','required'=>true,'enabled'=>true,'placeholder'=>'PCBA','col_width'=>0,'is_scan'=>false,'show_in_table'=>true,'table_label'=>'产品类型','fixed_value'=>'PCBA'],
['name'=>'product_model','label'=>'产品型号','type'=>'select','source'=>'product_models','required'=>true,'enabled'=>true,'placeholder'=>'请选择产品型号','col_width'=>2,'is_scan'=>false,'show_in_table'=>true,'table_label'=>'产品型号'],
['name'=>'static_power','label'=>'静态功耗','type'=>'number','required'=>false,'enabled'=>true,'placeholder'=>'0.0000','col_width'=>2,'is_scan'=>false,'show_in_table'=>true,'table_label'=>'静态功耗'],
['name'=>'input_output','label'=>'输入输出','type'=>'text','required'=>false,'enabled'=>true,'placeholder'=>'正常/异常','col_width'=>2,'is_scan'=>false,'show_in_table'=>true,'table_label'=>'输入输出'],
['name'=>'test_status','label'=>'测试状态','type'=>'select','source'=>'test_status_options','required'=>false,'enabled'=>true,'placeholder'=>'请选择','col_width'=>2,'is_scan'=>false,'show_in_table'=>true,'table_label'=>'测试状态'],
['name'=>'serial_no','label'=>'PCBA序列号','type'=>'text','required'=>true,'enabled'=>true,'placeholder'=>'扫描 PCBA 序列号后按回车','col_width'=>6,'is_scan'=>true,'show_in_table'=>true,'table_label'=>'PCBA序列号'],
],
'battery' => [
['name'=>'product_type','label'=>'类型','type'=>'hidden','required'=>true,'enabled'=>true,'placeholder'=>'电池','col_width'=>0,'is_scan'=>false,'show_in_table'=>true,'table_label'=>'类型','fixed_value'=>'电池'],
['name'=>'product_model','label'=>'产品型号','type'=>'select','source'=>'product_models','required'=>true,'enabled'=>true,'placeholder'=>'请选择产品型号','col_width'=>3,'is_scan'=>false,'show_in_table'=>true,'table_label'=>'产品型号'],
['name'=>'voltage_platform','label'=>'电压平台','type'=>'text','required'=>false,'enabled'=>true,'placeholder'=>'如:3.7V','col_width'=>2,'is_scan'=>false,'show_in_table'=>true,'table_label'=>'电压平台'],
['name'=>'no_load_voltage','label'=>'空载电压','type'=>'number','required'=>false,'enabled'=>true,'placeholder'=>'0.0000','col_width'=>2,'is_scan'=>false,'show_in_table'=>true,'table_label'=>'空载电压'],
['name'=>'internal_resistance','label'=>'电池内阻','type'=>'number','required'=>false,'enabled'=>true,'placeholder'=>'0.0000','col_width'=>2,'is_scan'=>false,'show_in_table'=>true,'table_label'=>'电池内阻'],
['name'=>'serial_no','label'=>'产品序列号','type'=>'text','required'=>true,'enabled'=>true,'placeholder'=>'扫描/输入后按回车','col_width'=>4,'is_scan'=>true,'show_in_table'=>true,'table_label'=>'产品序列号'],
],
'assembly' => [
['name'=>'product_model','label'=>'产品型号','type'=>'select','source'=>'product_models','required'=>true,'enabled'=>true,'placeholder'=>'请选择产品型号','col_width'=>2,'is_scan'=>false,'show_in_table'=>true,'table_label'=>'产品型号'],
['name'=>'shell_color','label'=>'外壳颜色','type'=>'select','source'=>'shell_color_options','required'=>false,'enabled'=>true,'placeholder'=>'请选择外壳颜色','col_width'=>2,'is_scan'=>false,'show_in_table'=>true,'table_label'=>'外壳颜色'],
['name'=>'finished_serial','label'=>'成品序列号','type'=>'text','required'=>true,'enabled'=>true,'placeholder'=>'扫描成品序列号','col_width'=>3,'is_scan'=>true,'show_in_table'=>true,'table_label'=>'成品序列号'],
['name'=>'battery_serial','label'=>'电池序列号','type'=>'text','required'=>true,'enabled'=>true,'placeholder'=>'扫描电池序列号','col_width'=>3,'is_scan'=>true,'show_in_table'=>true,'table_label'=>'电池序列号'],
['name'=>'pcb_serial','label'=>'PCB序列号','type'=>'text','required'=>true,'enabled'=>true,'placeholder'=>'扫描PCB序列号','col_width'=>2,'is_scan'=>true,'show_in_table'=>true,'table_label'=>'PCB序列号'],
],
'finished_test' => [
['name'=>'product_model','label'=>'产品型号','type'=>'select','source'=>'product_models','required'=>false,'enabled'=>true,'placeholder'=>'请选择产品型号','col_width'=>2,'is_scan'=>false,'show_in_table'=>true,'table_label'=>'产品型号'],
['name'=>'test_item','label'=>'测试项目','type'=>'text','required'=>true,'enabled'=>true,'placeholder'=>'如:功能测试','col_width'=>2,'is_scan'=>false,'show_in_table'=>true,'table_label'=>'测试项目'],
['name'=>'test_result','label'=>'测试结果','type'=>'select','source'=>'test_result_options','required'=>false,'enabled'=>true,'placeholder'=>'请选择','col_width'=>2,'is_scan'=>false,'show_in_table'=>true,'table_label'=>'测试结果'],
['name'=>'test_steps','label'=>'测试步骤','type'=>'text','required'=>false,'enabled'=>true,'placeholder'=>'步骤描述','col_width'=>2,'is_scan'=>false,'show_in_table'=>true,'table_label'=>'测试步骤'],
['name'=>'finished_serial','label'=>'成品序列号','type'=>'text','required'=>true,'enabled'=>true,'placeholder'=>'扫描序列号后按回车','col_width'=>4,'is_scan'=>true,'show_in_table'=>true,'table_label'=>'成品序列号'],
],
'warehouse' => [
['name'=>'product_model','label'=>'产品型号','type'=>'select','source'=>'product_models','required'=>false,'enabled'=>true,'placeholder'=>'请选择产品型号','col_width'=>3,'is_scan'=>false,'show_in_table'=>true,'table_label'=>'产品型号'],
['name'=>'finished_serial','label'=>'成品序列号','type'=>'text','required'=>true,'enabled'=>true,'placeholder'=>'扫描成品序列号后按回车','col_width'=>4,'is_scan'=>true,'show_in_table'=>true,'table_label'=>'成品序列号'],
['name'=>'box_serial','label'=>'箱序列号','type'=>'text','required'=>false,'enabled'=>true,'placeholder'=>'箱序列号','col_width'=>3,'is_scan'=>false,'show_in_table'=>true,'table_label'=>'箱序列号'],
],
'delivery' => [
['name'=>'customer_name','label'=>'客户名称','type'=>'select','source'=>'customers','required'=>true,'enabled'=>true,'placeholder'=>'请选择客户','col_width'=>3,'is_scan'=>false,'show_in_table'=>true,'table_label'=>'客户名称'],
['name'=>'finished_serial','label'=>'成品/箱序列号','type'=>'text','required'=>true,'enabled'=>true,'placeholder'=>'扫描序列号后按回车(自动识别型号)','col_width'=>4,'is_scan'=>true,'show_in_table'=>true,'table_label'=>'序列号'],
['name'=>'product_model','label'=>'产品型号','type'=>'hidden','required'=>false,'enabled'=>true,'placeholder'=>'','col_width'=>0,'is_scan'=>false,'show_in_table'=>true,'table_label'=>'产品型号'],
],
'semi_finished' => [
['name'=>'product_type','label'=>'类型','type'=>'hidden','required'=>true,'enabled'=>true,'placeholder'=>'半成品','col_width'=>0,'is_scan'=>false,'show_in_table'=>true,'table_label'=>'类型','fixed_value'=>'半成品'],
['name'=>'product_model','label'=>'产品型号','type'=>'select','source'=>'product_models','required'=>true,'enabled'=>true,'placeholder'=>'请选择产品型号','col_width'=>3,'is_scan'=>false,'show_in_table'=>true,'table_label'=>'产品型号'],
['name'=>'serial_no','label'=>'产品序列号','type'=>'text','required'=>true,'enabled'=>true,'placeholder'=>'扫描/输入后按回车','col_width'=>4,'is_scan'=>true,'show_in_table'=>true,'table_label'=>'产品序列号'],
],
'aging' => [
['name'=>'product_type','label'=>'类型','type'=>'hidden','required'=>true,'enabled'=>true,'placeholder'=>'半成品','col_width'=>0,'is_scan'=>false,'show_in_table'=>true,'table_label'=>'类型','fixed_value'=>'半成品'],
['name'=>'product_model','label'=>'产品型号','type'=>'select','source'=>'product_models','required'=>true,'enabled'=>true,'placeholder'=>'请选择产品型号','col_width'=>3,'is_scan'=>false,'show_in_table'=>true,'table_label'=>'产品型号'],
['name'=>'serial_no','label'=>'产品序列号','type'=>'text','required'=>true,'enabled'=>true,'placeholder'=>'扫描/输入后按回车','col_width'=>4,'is_scan'=>true,'show_in_table'=>true,'table_label'=>'产品序列号'],
['name'=>'aging_duration','label'=>'老化时长(h)','type'=>'number','required'=>false,'enabled'=>true,'placeholder'=>'如:24','col_width'=>2,'is_scan'=>false,'show_in_table'=>true,'table_label'=>'老化时长'],
['name'=>'aging_temp','label'=>'老化温度(℃)','type'=>'number','required'=>false,'enabled'=>true,'placeholder'=>'如:85','col_width'=>2,'is_scan'=>false,'show_in_table'=>true,'table_label'=>'老化温度'],
['name'=>'aging_result','label'=>'老化结果','type'=>'select','source'=>'test_result_options','required'=>false,'enabled'=>true,'placeholder'=>'请选择','col_width'=>2,'is_scan'=>false,'show_in_table'=>true,'table_label'=>'老化结果'],
],
];
$fields = isset($configs[$stationType]) ? $configs[$stationType] : [];
// ★ 关键修复:附加默认 _meta 信息
// 当 workstation.fields_config 为空时,FrontController::getStationFieldConfig()
// 会 fallback 到 getDefaultFieldsConfig(),但之前没有附加 _meta
// 导致 model_type_filter、type_filter 等元信息丢失。
$meta = getDefaultMetaConfig($stationType);
if (!empty($meta)) {
$fields['_meta'] = $meta;
}
return $fields;
}
/**
* 获取各工位类型的默认元信息配置
* 当数据库 fields_config 为空时作为兜底
*/
function getDefaultMetaConfig($stationType) {
$metas = [
'inbound' => [
'model_type_filter' => '半成品',
'type_filter' => 'inbound',
],
'pcb_test' => [
'model_type_filter' => 'PCBA',
],
'battery' => [
'model_type_filter' => '电池',
],
'assembly' => [
'model_type_filter' => '成品',
'extra_data_sources' => ['shellColors' => true],
],
'finished_test' => [
'model_type_filter' => '成品',
],
'warehouse' => [
'model_type_filter' => '成品',
],
'delivery' => [],
'semi_finished' => [
'model_type_filter' => '半成品',
],
'aging' => [
'model_type_filter' => '半成品',
],
];
return $metas[$stationType] ?? [];
}
/**
* 解析工位字段配置
* 返回数组,如果为空则使用默认配置
*
* JSON 格式支持两种结构:
* 1. 纯字段数组:[{name,label,...}, ...] (旧格式,兼容)
* 2. 元信息 + 字段:{"_meta":{...}, "fields":[{...}, ...]} (新格式)
*
* _meta 支持的字段:
* - model_type_filter: string 产品型号筛选类型(如 "成品"、"PCBA"、"电池"
* - type_filter: string 类型列表筛选方式(如 "inbound"、"enabled"
* - extra_data_sources: object 额外数据源配置
*/
function parseFieldsConfig($stationType, $fieldsConfigJson = '') {
if (!empty($fieldsConfigJson)) {
$config = json_decode($fieldsConfigJson, true);
if (is_array($config) && !empty($config)) {
// 新格式:{"_meta":{...}, "fields":[...]}
if (isset($config['_meta']) || isset($config['fields'])) {
$result = $config['fields'] ?? [];
$meta = $config['_meta'] ?? [];
// 用默认元信息补全缺失的 key(如 model_type_filter
$defaultMeta = getDefaultMetaConfig($stationType);
if (!empty($defaultMeta)) {
foreach ($defaultMeta as $k => $v) {
if (!array_key_exists($k, $meta)) {
$meta[$k] = $v;
}
}
}
$result['_meta'] = $meta;
return $result;
}
// 旧格式:纯字段数组 — 附加默认元信息
$meta = getDefaultMetaConfig($stationType);
if (!empty($meta)) {
$config['_meta'] = $meta;
}
return $config;
}
}
// 回退到默认配置,附加默认元信息
$defaultFields = getDefaultFieldsConfig($stationType);
$meta = getDefaultMetaConfig($stationType);
if (!empty($meta)) {
$defaultFields['_meta'] = $meta;
}
return $defaultFields;
}
/**
* 渲染前端表单输入字段 (根据配置动态生成)
* $fieldConfigs: 解析后的字段配置数组
* $dataSources: ['types' => [], 'models' => [], 'customers' => []]
* $isReadonly: 操作人显示名称
*/
function renderStationFields($fieldConfigs, $dataSources = [], $operatorName = '', $excludeFields = []) {
$html = '';
$enabledFields = array_filter($fieldConfigs, function($f) { return !empty($f['enabled']); });
// 排除指定字段
if (!empty($excludeFields)) {
$enabledFields = array_filter($enabledFields, function($f) use ($excludeFields) {
return !in_array($f['name'], $excludeFields);
});
}
// 分组:扫描字段和其他字段
$scanFields = array_filter($enabledFields, function($f) { return !empty($f['is_scan']); });
$normalFields = array_filter($enabledFields, function($f) { return empty($f['is_scan']); });
// 输出所有非扫描字段 + 操作人(CSS Grid 自适应布局)
$html .= '<div class="station-fields-grid" id="stationFieldsGrid">';
foreach ($normalFields as $field) {
$html .= '<div class="station-field-item">';
$html .= renderSingleField($field, $dataSources);
$html .= '</div>';
}
// 操作人(固定显示)
if ($operatorName) {
$html .= '<div class="station-field-item">';
$html .= '<label>操作人</label>';
$html .= '<input type="text" class="form-control" value="' . htmlspecialchars($operatorName) . '" disabled>';
$html .= '</div>';
}
$html .= '</div>';
// 扫描字段(序列号类)- 单独一行,宽度自适应
if (!empty($scanFields)) {
$html .= '<div class="station-scan-grid" style="margin-top:15px;">';
foreach ($scanFields as $field) {
$html .= '<div class="station-field-item station-scan-item">';
$html .= renderSingleField($field, $dataSources, true);
$html .= '</div>';
}
$html .= '</div>';
}
return $html;
}
/**
* 渲染单个字段
*/
function renderSingleField($field, $dataSources = [], $isScan = false) {
$name = htmlspecialchars($field['name']);
$label = htmlspecialchars($field['label']);
$type = $field['type'] ?? 'text';
$required = !empty($field['required']) ? 'required' : '';
$placeholder = htmlspecialchars($field['placeholder'] ?? '');
$html = '';
// hidden 类型:只输出隐藏域,不显示标签
if ($type === 'hidden') {
$fixedValue = htmlspecialchars($field['fixed_value'] ?? '');
$html = '<input type="hidden" name="' . $name . '" value="' . $fixedValue . '">';
return $html;
}
$html .= '<label>' . $label;
if ($required) $html .= ' <span class="text-danger">*</span>';
if ($isScan) $html .= ' <small class="text-muted">(回车添加)</small>';
$html .= '</label>';
if ($type === 'select') {
$source = $field['source'] ?? '';
$cssClass = $isScan ? 'form-control scan-submit' : 'form-control sticky-save';
$html .= '<select name="' . $name . '" class="' . $cssClass . '" ' . $required . '>';
$html .= '<option value="">' . $placeholder . '</option>';
if ($source === 'type_list' && isset($dataSources['types'])) {
// 去重(防止重复的类型名称)
$seen = [];
foreach ($dataSources['types'] as $type) {
// 兼容多种数据源格式:
// - product_type_config 表:type_name 列
// - ProductModel.getTypes()type 列
// - ProductModel.getProductTypes()product_type 列
$val = htmlspecialchars($type['type_name'] ?? $type['type'] ?? $type['product_type'] ?? '');
if ($val === '' || isset($seen[$val])) continue;
$seen[$val] = true;
$html .= '<option value="' . $val . '">' . $val . '</option>';
}
} elseif ($source === 'product_types' && isset($dataSources['types'])) {
$seen = [];
foreach ($dataSources['types'] as $type) {
$val = htmlspecialchars($type['product_type'] ?? '');
if ($val === '' || isset($seen[$val])) continue;
$seen[$val] = true;
$html .= '<option value="' . $val . '">' . $val . '</option>';
}
} elseif ($source === 'product_models' && isset($dataSources['models'])) {
if (empty($dataSources['models'])) {
// 没有传入型号列表时,显示提示(由前端联动动态加载)
$html .= '<option value="">请先选择产品类型</option>';
} else {
foreach ($dataSources['models'] as $model) {
$val = htmlspecialchars($model['model_code']);
$html .= '<option value="' . $val . '">' . $val . '</option>';
}
}
} elseif ($source === 'customers' && isset($dataSources['customers'])) {
foreach ($dataSources['customers'] as $cust) {
$val = htmlspecialchars($cust['company_name'] ?? '');
$html .= '<option value="' . $val . '">' . $val . '</option>';
}
} elseif ($source === 'test_status_options') {
$html .= '<option value="pending">待测试</option>';
$html .= '<option value="passed">通过</option>';
$html .= '<option value="failed">失败</option>';
} elseif ($source === 'test_result_options') {
$html .= '<option value="通过">通过</option>';
$html .= '<option value="失败">失败</option>';
$html .= '<option value="未测试">未测试</option>';
} elseif ($source === 'shell_color_options') {
if (isset($dataSources['shell_colors']) && !empty($dataSources['shell_colors'])) {
foreach ($dataSources['shell_colors'] as $color) {
$val = htmlspecialchars($color['model_code']);
$html .= '<option value="' . $val . '">' . $val . '</option>';
}
} else {
$html .= '<option value="">暂无外壳颜色数据</option>';
}
}
$html .= '</select>';
} elseif ($type === 'number') {
$cssClass = $isScan ? 'form-control scan-submit' : 'form-control sticky-save';
$html .= '<input type="number" step="any" name="' . $name . '" class="' . $cssClass . '" placeholder="' . $placeholder . '" ' . $required . '>';
} elseif ($type === 'textarea') {
$html .= '<textarea name="' . $name . '" class="form-control sticky-save" placeholder="' . $placeholder . '" rows="2" ' . $required . '></textarea>';
} else {
// text 类型
$cssClass = $isScan ? 'form-control scan-submit' : 'form-control sticky-save';
$autofocus = $isScan ? ' autofocus' : '';
$html .= '<input type="text" name="' . $name . '" class="' . $cssClass . '" placeholder="' . $placeholder . '" ' . $required . $autofocus . '>';
}
return $html;
}
/**
* 渲染记录表格表头
*/
function renderRecordTableHeader($fieldConfigs) {
$html = '<th data-col="seq">序号</th>';
$colIdx = 0;
foreach ($fieldConfigs as $field) {
if (!empty($field['enabled']) && !empty($field['show_in_table'])) {
$colKey = htmlspecialchars($field['name']);
$html .= '<th data-col="' . $colKey . '">' . htmlspecialchars($field['table_label'] ?? $field['label']) . '</th>';
$colIdx++;
}
}
$html .= '<th data-col="operator">操作人</th><th data-col="time">时间</th>';
return $html;
}
/**
* 渲染记录表格数据行
*/
function renderRecordTableRow($record, $fieldConfigs, $specialFormats = []) {
$html = '<tr>';
$html .= '<td data-col="seq">' . $record['id'] . '</td>';
foreach ($fieldConfigs as $field) {
if (empty($field['enabled']) || empty($field['show_in_table'])) continue;
$name = $field['name'];
$val = $record[$name] ?? '';
$colKey = htmlspecialchars($name);
if (isset($specialFormats[$name])) {
$html .= '<td data-col="' . $colKey . '">' . $specialFormats[$name]($val, $record) . '</td>';
} else {
$html .= '<td data-col="' . $colKey . '">' . htmlspecialchars($val) . '</td>';
}
}
$html .= '<td data-col="operator">' . htmlspecialchars($record['operator'] ?? '') . '</td>';
// 兼容各种时间列名:created_at, create_time, assembly_time, test_time, delivery_time, in_time
$timeValue = $record['created_at']
?? $record['create_time']
?? $record['assembly_time']
?? $record['test_time']
?? $record['delivery_time']
?? $record['in_time']
?? '';
$html .= '<td data-col="time">' . htmlspecialchars($timeValue) . '</td>';
$html .= '</tr>';
return $html;
}
/**
* 渲染表格列选择器(下拉多选按钮)
* @param string $pageKey 页面唯一标识,用于 localStorage 区分不同页面
* @param array $fieldConfigs 字段配置数组
* @return string HTML
*/
function renderTableColumnSelector($pageKey, $fieldConfigs) {
$cols = [['key' => 'seq', 'label' => '序号']];
foreach ($fieldConfigs as $field) {
if (!empty($field['enabled']) && !empty($field['show_in_table'])) {
$cols[] = [
'key' => $field['name'],
'label'=> $field['table_label'] ?? $field['label'],
];
}
}
$cols[] = ['key' => 'operator', 'label' => '操作人'];
$cols[] = ['key' => 'time', 'label' => '时间'];
$colsJson = json_encode($cols, JSON_UNESCAPED_UNICODE);
$pageKeyJson = json_encode($pageKey);
$html = '<span class="table-column-selector" title="选择显示列" style="margin-left:8px;">';
$html .= '<button type="button" class="col-btn col-btn-dropdown" id="colSelectBtn_' . htmlspecialchars($pageKey) . '">';
$html .= '<i class="fa fa-columns"></i> 列选择 <i class="fa fa-caret-down"></i>';
$html .= '</button>';
$html .= '<div class="col-dropdown-panel" id="colDropdown_' . htmlspecialchars($pageKey) . '" style="display:none;">';
$html .= '<div class="col-dropdown-header">';
$html .= '<label><input type="checkbox" class="col-check-all" checked> 全选</label>';
$html .= '<button type="button" class="col-reset-btn" title="恢复默认"><i class="fa fa-refresh"></i></button>';
$html .= '</div>';
$html .= '<div class="col-dropdown-list">';
foreach ($cols as $col) {
$html .= '<label><input type="checkbox" class="col-check-item" data-col="' . htmlspecialchars($col['key']) . '" checked> '
. htmlspecialchars($col['label']) . '</label>';
}
$html .= '</div>';
$html .= '</div>';
$html .= '</span>';
// 内联初始化脚本(在元素渲染后立即执行)
$html .= '<script nonce="' . csp_nonce() . '">window._colSelectorQueue = window._colSelectorQueue || [];';
$html .= 'window._colSelectorQueue.push({pageKey:' . $pageKeyJson . ',cols:' . $colsJson . '});</script>';
return $html;
}
/**
* 生成字段配置编辑UI的JSON字符串(供JS使用)
*/
function renderFieldConfigEditor($stationType, $fieldsConfigJson = '', $stationDefinition = null) {
$rawConfig = parseFieldsConfig($stationType, $fieldsConfigJson);
// 分离 _meta 和 fields
$meta = $rawConfig['_meta'] ?? [];
$fields = $rawConfig;
unset($fields['_meta']);
$fields = array_values($fields);
$fieldsJson = json_encode($fields, JSON_UNESCAPED_UNICODE);
$metaJson = json_encode($meta, JSON_UNESCAPED_UNICODE);
$stationTypeJson = json_encode($stationType);
// 工位定义元数据(用于数据表结构管控)
$stationDef = $stationDefinition ?: [];
$hasProductType = !empty($stationDef['has_product_type']);
$hasProductModel = !empty($stationDef['has_product_model']);
$filterEnabled = !empty($stationDef['filter_enabled']);
$showTodayCount = !empty($stationDef['show_today_count']);
$showTotalCount = !empty($stationDef['show_total_count']);
$defId = $stationDef['id'] ?? 0;
// 产品类型选项(用于型号筛选下拉)
$productTypeOptions = json_encode(['', '半成品', '成品', 'PCBA', '电池', '外壳', '外壳颜色'], JSON_UNESCAPED_UNICODE);
ob_start();
?>
<div id="fieldConfigEditor" style="margin-top:15px; border-top:1px solid #eee; padding-top:15px;">
<h4><i class="fa fa-cog"></i> 输入数据项配置
<button type="button" class="btn btn-xs btn-success" id="addFieldBtn"><i class="fa fa-plus"></i> 添加字段</button>
<button type="button" class="btn btn-xs btn-warning" id="resetDefaultsBtn"><i class="fa fa-refresh"></i> 恢复默认</button>
</h4>
<!-- 工位定义元数据配置区(管控数据表结构) -->
<div class="panel panel-default" style="margin-bottom:15px;">
<div class="panel-heading"><i class="fa fa-database"></i> 数据表结构管控
<?php if ($defId): ?><span class="pull-right text-muted" style="font-size:12px;">工位定义 ID: <?php echo $defId; ?></span><?php endif; ?>
</div>
<div class="panel-body">
<div class="row">
<div class="col-md-4">
<label>
<input type="checkbox" name="sd_has_product_type" value="1" <?php echo $hasProductType ? 'checked' : ''; ?>>
数据表含 product_type 列
</label>
<br><small class="text-muted">记录可按产品类型筛选</small>
</div>
<div class="col-md-4">
<label>
<input type="checkbox" name="sd_has_product_model" value="1" <?php echo $hasProductModel ? 'checked' : ''; ?>>
数据表含 product_model 列
</label>
<br><small class="text-muted">记录可按产品型号筛选</small>
</div>
<div class="col-md-4">
<label>
<input type="checkbox" name="sd_filter_enabled" value="1" <?php echo $filterEnabled ? 'checked' : ''; ?>>
启用前端筛选
</label>
<br><small class="text-muted">前端显示类型/型号筛选联动</small>
</div>
</div>
<div class="row" style="margin-top:10px;">
<div class="col-md-4">
<label>
<input type="checkbox" name="sd_show_today_count" value="1" <?php echo $showTodayCount ? 'checked' : ''; ?>>
显示今日录入计数
</label>
</div>
<div class="col-md-4">
<label>
<input type="checkbox" name="sd_show_total_count" value="1" <?php echo $showTotalCount ? 'checked' : ''; ?>>
显示总记录计数
</label>
</div>
</div>
</div>
</div>
<!-- 元信息配置区 -->
<div class="panel panel-default" style="margin-bottom:15px;">
<div class="panel-heading"><i class="fa fa-sliders"></i> 数据筛选设置</div>
<div class="panel-body">
<div class="row">
<div class="col-md-4">
<label>产品型号筛选类型
<select class="form-control input-sm" id="metaModelTypeFilter">
<option value="">不限(显示全部型号)</option>
</select>
</label>
<small class="text-muted">选择后产品型号下拉只显示该类型</small>
</div>
<div class="col-md-4">
<label>类型列表筛选方式
<select class="form-control input-sm" id="metaTypeFilter">
<option value="">默认(所有启用的类型)</option>
<option value="inbound">入库可见类型</option>
<option value="enabled">已启用类型</option>
</select>
</label>
<small class="text-muted">控制"类型"下拉框显示哪些选项</small>
</div>
<div class="col-md-4">
<label>
<input type="checkbox" id="metaShellColors">
启用外壳颜色数据源
</label>
<br><small class="text-muted">组装工位使用,加载外壳颜色选项</small>
</div>
</div>
</div>
</div>
<!-- 序列号前缀映射区 -->
<div class="panel panel-default" style="margin-bottom:15px;">
<div class="panel-heading"><i class="fa fa-shield"></i> 序列号前缀规则(防错)
<span class="pull-right text-muted" style="font-size:12px;">在 <a href="<?php echo BASE_URL; ?>/Admin/sysConfig" target="_blank">系统配置 → 序列号前缀规则</a> 中管理前缀</span>
</div>
<div class="panel-body">
<p class="text-muted" style="margin-bottom:10px;">
<i class="fa fa-info-circle"></i>
为扫描字段(is_scan=true)指定对应的产品类型,提交时将自动验证序列号前缀是否正确。<br>
选择「动态匹配」则根据提交时选择的 product_type 自动匹配前缀。
</p>
<div id="prefixFieldMapArea" style="max-height:200px; overflow-y:auto;">
<table class="table table-condensed table-bordered" style="margin-bottom:0;">
<thead>
<tr>
<th style="width:30%">扫描字段</th>
<th style="width:40%">对应产品类型</th>
<th style="width:30%">预期前缀</th>
</tr>
</thead>
<tbody id="prefixFieldMapTbody">
</tbody>
</table>
</div>
<small class="text-muted" style="display:block;margin-top:5px;">
提示:留空则自动从工位的 product_type 字段推断(如有 hidden 类型的 product_type 则使用其 fixed_value,否则动态匹配)
</small>
</div>
</div>
<div class="table-responsive">
<table class="table table-bordered table-hover" id="fieldsTable" style="margin-bottom:0;">
<thead>
<tr>
<th style="width:3%">启用</th>
<th style="width:10%">字段名</th>
<th style="width:10%">显示标签</th>
<th style="width:8%">类型</th>
<th style="width:10%">数据源</th>
<th style="width:4%">必填</th>
<th style="width:4%">唯一</th>
<th style="width:10%">占位提示</th>
<th style="width:5%">列宽</th>
<th style="width:5%">扫码</th>
<th style="width:5%">表中</th>
<th style="width:10%">表头标签</th>
<th style="width:5%">操作</th>
</tr>
</thead>
<tbody id="fieldsTbody"></tbody>
</table>
</div>
<input type="hidden" name="fields_config" id="fieldsConfigInput" value="">
<small class="text-muted"><i class="fa fa-info-circle"></i> 勾选「扫码」的字段会以扫码枪回车提交方式处理。勾选「表中」会在记录表格中显示该列。</small>
</div>
<script nonce="<?php echo csp_nonce(); ?>">
var stationType = <?php echo $stationTypeJson; ?>;
var defaultFields = <?php echo $fieldsJson; ?>;
var currentFields = JSON.parse(JSON.stringify(defaultFields));
var currentMeta = <?php echo $metaJson; ?>;
var productTypeOptions = <?php echo $productTypeOptions; ?>;
var fieldTypes = [
{v:'text',n:'文本'},
{v:'number',n:'数字'},
{v:'select',n:'下拉选择'},
{v:'textarea',n:'多行文本'}
];
var fieldSources = [
{v:'',n:'无(手动输入)'},
{v:'product_types',n:'产品类型列表'},
{v:'product_models',n:'产品型号列表'},
{v:'customers',n:'客户列表'},
{v:'test_status_options',n:'测试状态(待测/通过/失败)'},
{v:'test_result_options',n:'测试结果(通过/失败/未测试)'},
{v:'shell_color_options',n:'外壳颜色选项'}
];
// 初始化元信息下拉框
function initMetaUI() {
var sel = document.getElementById('metaModelTypeFilter');
sel.innerHTML = '<option value="">不限(显示全部型号)</option>';
productTypeOptions.forEach(function(t) {
if (!t) return;
sel.innerHTML += '<option value="' + t + '"' + (currentMeta.model_type_filter === t ? ' selected' : '') + '>' + t + '</option>';
});
document.getElementById('metaTypeFilter').value = currentMeta.type_filter || '';
document.getElementById('metaShellColors').checked = !!(currentMeta.extra_data_sources && currentMeta.extra_data_sources.shellColors);
// 渲染前缀映射表
renderPrefixFieldMap();
}
// ===== 前缀字段映射渲染 =====
// 从 currentMeta.prefix_field_map 读取映射,为每个 is_scan 的 text 字段渲染一行
function renderPrefixFieldMap() {
var tbody = document.getElementById('prefixFieldMapTbody');
if (!tbody) return;
var prefixMap = currentMeta.prefix_field_map || {};
// 用已知前缀规则展示预期前缀预览
var knownPrefixes = {};
try {
// productTypeOptions 中包含产品类型列表,前缀从服务器端已知规则获取
// 这里只做展示,实际规则由后端 SysConfig 动态读取
} catch(e) {}
var html = '';
currentFields.forEach(function(f, i) {
if (!f.enabled || !f.is_scan || f.type !== 'text') return;
var fieldName = f.name || '';
if (!fieldName) return;
var currentVal = prefixMap[fieldName] || '';
var label = f.label || fieldName;
// 构建产品类型下拉选项
var typeOpts = '<option value="">(自动推断)</option>';
typeOpts += '<option value="*dynamic*"' + (currentVal === '*dynamic*' ? ' selected' : '') + '>动态匹配(根据 product_type</option>';
productTypeOptions.forEach(function(t) {
if (!t) return;
typeOpts += '<option value="' + t + '"' + (currentVal === t ? ' selected' : '') + '>' + t + '</option>';
});
html += '<tr>';
html += '<td><strong>' + escHtml(label) + '</strong><br><small class="text-muted">' + escHtml(fieldName) + '</small></td>';
html += '<td><select class="form-control input-sm" data-prefix-field="' + escHtml(fieldName) + '" style="min-width:160px;">' + typeOpts + '</select></td>';
html += '<td><span class="text-muted" style="font-size:12px;">' + (currentVal && currentVal !== '*dynamic*' ? '系统配置中定义' : currentVal === '*dynamic*' ? '运行时动态匹配' : '自动推断') + '</span></td>';
html += '</tr>';
});
if (html === '') {
html = '<tr><td colspan="3" class="text-muted" style="text-align:center;padding:20px;">暂无扫描字段。勾选字段的「扫码」选项后,此处将显示前缀映射配置。</td></tr>';
}
tbody.innerHTML = html;
}
function updatePrefixMap(fieldName, productType) {
if (!currentMeta.prefix_field_map) currentMeta.prefix_field_map = {};
if (productType) {
currentMeta.prefix_field_map[fieldName] = productType;
} else {
delete currentMeta.prefix_field_map[fieldName];
}
updateFieldsConfig();
// 刷新前缀列显示
renderPrefixFieldMap();
}
// 更新隐藏域值(新格式:{"_meta":{...}, "fields":[...]}
function updateFieldsConfig() {
var config = {_meta: currentMeta, fields: currentFields};
document.getElementById('fieldsConfigInput').value = JSON.stringify(config);
}
function resetDefaults() {
if (confirm('确定恢复为默认配置吗?当前配置将丢失。')) {
currentFields = JSON.parse(JSON.stringify(defaultFields));
currentMeta = {};
initMetaUI();
renderFieldsTable();
renderPrefixFieldMap();
updateFieldsConfig();
}
}
function addField() {
currentFields.push({
name: '', label: '', type: 'text', source: '', required: false, unique: false,
enabled: true, placeholder: '', col_width: 2, is_scan: false,
show_in_table: true, table_label: ''
});
renderFieldsTable();
}
function removeField(idx) {
if (confirm('确定删除这个字段吗?')) {
currentFields = currentFields.filter(function(_, i) { return i !== idx; });
renderFieldsTable();
}
}
function moveField(idx, dir) {
var newIdx = idx + dir;
if (newIdx < 0 || newIdx >= currentFields.length) return;
var tmp = currentFields[idx];
currentFields[idx] = currentFields[newIdx];
currentFields[newIdx] = tmp;
renderFieldsTable();
}
function renderFieldsTable() {
var tbody = document.getElementById('fieldsTbody');
var html = '';
currentFields.forEach(function(f, i) {
var typeOpts = fieldTypes.map(function(t) {
return '<option value="' + t.v + '"' + (f.type === t.v ? ' selected' : '') + '>' + t.n + '</option>';
}).join('');
var sourceOpts = fieldSources.map(function(s) {
return '<option value="' + s.v + '"' + ((f.source||'') === s.v ? ' selected' : '') + '>' + s.n + '</option>';
}).join('');
html += '<tr>';
html += '<td style="text-align:center"><input type="checkbox" data-idx="'+i+'" data-prop="enabled" ' + (f.enabled !== false ? 'checked' : '') + '></td>';
html += '<td><input type="text" class="form-control input-sm" value="' + escHtml(f.name) + '" data-idx="'+i+'" data-prop="name" placeholder="字段名" style="min-width:90px;"></td>';
html += '<td><input type="text" class="form-control input-sm" value="' + escHtml(f.label) + '" data-idx="'+i+'" data-prop="label" placeholder="标签" style="min-width:90px;"></td>';
html += '<td><select class="form-control input-sm" data-idx="'+i+'" data-prop="type">' + typeOpts + '</select></td>';
html += '<td><select class="form-control input-sm" data-idx="'+i+'" data-prop="source">' + sourceOpts + '</select></td>';
html += '<td style="text-align:center"><input type="checkbox" data-idx="'+i+'" data-prop="required" ' + (f.required ? 'checked' : '') + '></td>';
html += '<td style="text-align:center"><input type="checkbox" data-idx="'+i+'" data-prop="unique" title="不允许重复录入" ' + (f.unique ? 'checked' : '') + '></td>';
html += '<td><input type="text" class="form-control input-sm" value="' + escHtml(f.placeholder) + '" data-idx="'+i+'" data-prop="placeholder" placeholder="提示" style="min-width:90px;"></td>';
html += '<td><input type="number" class="form-control input-sm" value="' + (f.col_width||2) + '" min="1" max="12" data-idx="'+i+'" data-prop="col_width" style="width:55px;"></td>';
html += '<td style="text-align:center"><input type="checkbox" data-idx="'+i+'" data-prop="is_scan" ' + (f.is_scan ? 'checked' : '') + '></td>';
html += '<td style="text-align:center"><input type="checkbox" data-idx="'+i+'" data-prop="show_in_table" ' + (f.show_in_table !== false ? 'checked' : '') + '></td>';
html += '<td><input type="text" class="form-control input-sm" value="' + escHtml(f.table_label||'') + '" data-idx="'+i+'" data-prop="table_label" placeholder="表头" style="min-width:90px;"></td>';
html += '<td style="text-align:center;white-space:nowrap">';
html += '<button type="button" class="btn btn-xs btn-default" data-idx="'+i+'" data-faction="moveUp" title="上移"><i class="fa fa-arrow-up"></i></button> ';
html += '<button type="button" class="btn btn-xs btn-default" data-idx="'+i+'" data-faction="moveDown" title="下移"><i class="fa fa-arrow-down"></i></button> ';
html += '<button type="button" class="btn btn-xs btn-danger" data-idx="'+i+'" data-faction="remove" title="删除"><i class="fa fa-trash"></i></button>';
html += '</td>';
html += '</tr>';
});
tbody.innerHTML = html;
updateFieldsConfig();
}
function escHtml(s) { return (s||'').replace(/&/g,'&amp;').replace(/"/g,'&quot;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }
// ===== CSP 事件绑定(替代内联 onclick/onchange 处理器)=====
function bindFieldConfigEvents() {
// 顶部操作按钮
var addBtn = document.getElementById('addFieldBtn');
if (addBtn) addBtn.addEventListener('click', addField);
var resetBtn = document.getElementById('resetDefaultsBtn');
if (resetBtn) resetBtn.addEventListener('click', resetDefaults);
// 元信息筛选控件
var mmf = document.getElementById('metaModelTypeFilter');
if (mmf) mmf.addEventListener('change', function () {
currentMeta.model_type_filter = this.value; updateFieldsConfig();
});
var mtf = document.getElementById('metaTypeFilter');
if (mtf) mtf.addEventListener('change', function () {
currentMeta.type_filter = this.value; updateFieldsConfig();
});
var msc = document.getElementById('metaShellColors');
if (msc) msc.addEventListener('change', function () {
currentMeta.extra_data_sources = currentMeta.extra_data_sources || {};
currentMeta.extra_data_sources.shellColors = this.checked;
updateFieldsConfig();
});
// 字段表格:事件委托(绑定在稳定的 tbody 上,innerHTML 重渲染后依然有效)
var tbody = document.getElementById('fieldsTbody');
if (tbody) {
tbody.addEventListener('change', function (e) {
var el = e.target;
var idxAttr = el.getAttribute && el.getAttribute('data-idx');
var prop = el.getAttribute && el.getAttribute('data-prop');
if (idxAttr === null || idxAttr === undefined || !prop) return;
var idx = parseInt(idxAttr, 10);
var f = currentFields[idx];
if (!f) return;
if (el.type === 'checkbox') {
f[prop] = el.checked;
} else if (prop === 'col_width') {
f[prop] = parseInt(el.value, 10) || 2;
} else {
f[prop] = el.value;
}
updateFieldsConfig();
if (prop === 'type') renderFieldsTable();
if (prop === 'is_scan') renderPrefixFieldMap();
});
tbody.addEventListener('click', function (e) {
var btn = e.target.closest ? e.target.closest('button[data-faction]') : null;
if (!btn) return;
var idx = parseInt(btn.getAttribute('data-idx'), 10);
var action = btn.getAttribute('data-faction');
if (action === 'moveUp') moveField(idx, -1);
else if (action === 'moveDown') moveField(idx, 1);
else if (action === 'remove') removeField(idx);
});
}
// 前缀映射表:事件委托
var prefixTbody = document.getElementById('prefixFieldMapTbody');
if (prefixTbody) {
prefixTbody.addEventListener('change', function (e) {
var el = e.target;
var fieldName = el.getAttribute && el.getAttribute('data-prefix-field');
if (!fieldName) return;
updatePrefixMap(fieldName, el.value);
});
}
}
// 表单提交前更新隐藏域
document.addEventListener('DOMContentLoaded', function() {
initMetaUI();
renderFieldsTable();
bindFieldConfigEvents();
var form = document.querySelector('form[method="post"]');
if (form) {
form.addEventListener('submit', function() {
updateFieldsConfig();
});
}
});
</script>
<?php
return ob_get_clean();
}
/**
* 生成扫描字段连环回车 JS 代码
*
* 行为:
* - 扫描字段按 Enter → 自动填入扫码值 → 焦点跳到下一个空扫描字段
* - 所有扫描字段填满后按 Enter → 触发提交
* - 非扫描字段按 Enter → 焦点自然跳下一个输入框(tabindex 顺序)
*
* @param string $formSelector 表单选择器,默认 '#mainForm'
* @param string $submitSelector 提交按钮选择器(可选),留空则直接 submit 表单
* @param bool $clearAfterSubmit 提交后是否清空扫描字段(默认true)
*/
function renderScanChainJS($formSelector = '#mainForm', $submitSelector = '', $clearAfterSubmit = true) {
ob_start();
?>
function initScanChain() {
var $form = $('<?php echo $formSelector; ?>');
if (!$form.length) return;
// 找到表单内所有输入元素(按DOM顺序)
function getInputsInOrder() {
var inputs = [];
$form.find('input:not([disabled]):not([type="hidden"]):not(:submit):not(:button), select:not([disabled]), textarea:not([disabled])').each(function() {
var $el = $(this);
if ($el.closest('#fieldConfigEditor').length) return; // 跳过配置编辑器
inputs.push(this);
});
// 按tabindex排序(如果有的话),否则按DOM顺序
inputs.sort(function(a, b) {
var ta = parseInt($(a).attr('tabindex')) || 0;
var tb = parseInt($(b).attr('tabindex')) || 0;
if (ta && tb) return ta - tb;
if (ta) return -1;
if (tb) return 1;
return 0;
});
return inputs;
}
// 检查是否所有扫描字段都已填值
function allScanFieldsFilled() {
var allFilled = true;
$form.find('.scan-submit').each(function() {
if ($(this).val().trim() === '') allFilled = false;
});
return allFilled;
}
function doSubmit() {
<?php if ($submitSelector): ?>
$('<?php echo $submitSelector; ?>').click();
<?php else: ?>
$form.submit();
<?php endif; ?>
}
// 回车事件:扫描字段 → 跳到下一个空位;最后一位 → 提交
$form.on('keypress', '.scan-submit', function(e) {
if (e.which !== 13) return;
e.preventDefault();
var $this = $(this);
var val = $this.val().trim();
if (val === '') return; // 空值不处理
// 找到所有扫描字段
var scanInputs = [];
$form.find('.scan-submit').each(function() { scanInputs.push(this); });
// 如果所有扫描字段都填满了 → 提交
if (allScanFieldsFilled()) {
doSubmit();
return;
}
// 找到下一个空的扫描字段
var found = false;
for (var i = 0; i < scanInputs.length; i++) {
if (scanInputs[i] === this) {
// 从当前之后找第一个空的
for (var j = 1; j < scanInputs.length; j++) {
var idx = (i + j) % scanInputs.length;
if ($(scanInputs[idx]).val().trim() === '') {
$(scanInputs[idx]).focus().select();
found = true;
break;
}
}
break;
}
}
if (!found) {
// 没找到空的,提交
doSubmit();
}
});
// 非扫描字段的回车 → 跳到下一个输入框
$form.on('keypress', 'input:not(.scan-submit):not([type="hidden"]):not(:submit):not(:button), select, textarea', function(e) {
if (e.which !== 13) return;
var $el = $(this);
if ($el.closest('#fieldConfigEditor').length) return;
e.preventDefault();
var inputs = getInputsInOrder();
var idx = inputs.indexOf(this);
if (idx >= 0 && idx < inputs.length - 1) {
$(inputs[idx + 1]).focus().select();
}
});
// 提交后清空扫描字段(重新聚焦第一个)
$form.on('submit', function() {
<?php if ($clearAfterSubmit): ?>
setTimeout(function() {
var $firstScan = $form.find('.scan-submit').first();
if ($firstScan.length) $firstScan.focus();
}, 100);
<?php endif; ?>
});
}
$(function() { initScanChain(); });
<?php
return ob_get_clean();
}