初始化

This commit is contained in:
2026-08-08 18:28:49 +08:00
parent 9bef4420e0
commit f082037a6e
854 changed files with 217171 additions and 11 deletions
+975
View File
@@ -0,0 +1,975 @@
<?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();
}
+125
View File
@@ -0,0 +1,125 @@
<?php
/**
* 工位业务配置辅助文件
*
* 将所有工位的特殊业务逻辑抽象为配置驱动。
* 每个内置工位在此定义其业务配置,FrontController 的 __call 统一读取并执行。
*
* 新增/修改工位只需:
* 1. 在 Admin 后台添加/编辑 workstation 记录
* 2. 如有特殊业务逻辑(如独立数据表、自定义提交),在此文件中添加配置
* 3. 无需修改 FrontController 或创建新视图文件
*/
/**
* 从 SysConfig 动态生成 codePrefix 验证规则
*
* 配置格式(在工位配置中):
* 'codePrefixFields' => [
* '字段名' => '产品类型', // 如 'serial_no' => 'PCBA'
* ]
*
* 运行时从 sys_config.code_prefix_rules 读取产品类型对应的前缀,
* 自动生成 codePrefix 验证规则。如果该产品类型未配置前缀(空字符串),则跳过验证。
*
* @param array|null $fieldMappings 字段到产品类型的映射
* @return array 格式化后的 codePrefix 规则
*/
function buildCodePrefixRules($fieldMappings, $prefixTypeMap = null) {
if (empty($fieldMappings)) return [];
// 如果未传入前缀映射,从 SysConfig 读取(兼容旧用法)
if ($prefixTypeMap === null) {
try {
$config = new \app\models\SysConfig();
$prefixTypeMap = $config->getCodePrefixRules();
} catch (\Throwable $e) {
error_log('[buildCodePrefixRules] getCodePrefixRules failed: ' . $e->getMessage());
$prefixTypeMap = [];
}
}
if (!is_array($prefixTypeMap)) $prefixTypeMap = [];
$rules = [];
foreach ($fieldMappings as $fieldName => $productType) {
// 动态前缀:*dynamic* 标记表示根据提交时的 product_type 动态匹配
if ($productType === '*dynamic*') {
$rules[$fieldName] = ['*dynamic*', '序列号前缀与产品类型不匹配'];
continue;
}
$prefix = $prefixTypeMap[$productType] ?? '';
// 原则:所有工位序列号都受前缀管控。类型已配置但前缀为空时,
// 自动回退为产品类型名拼音首字母(如 外壳→WK、包装→BZ、半成品→BCP、配件→PJ)。
if ($prefix === '') {
try {
$sc = new \app\models\SysConfig();
$prefix = $sc->pinyinInitials($productType);
} catch (\Throwable $e) {
error_log('[buildCodePrefixRules] pinyinInitials failed: ' . $e->getMessage());
$prefix = '';
}
}
if ($prefix === '') continue; // 实在无法解析(如类型根本未配置)才跳过
$rules[$fieldName] = [$prefix, "{$productType}序列号必须以 {$prefix} 开头"];
}
return $rules;
}
/**
* 获取指定工位的业务配置
* @param string $stationType 工位类型标识
* @return array|null 业务配置数组,null 表示使用通用 station_records 存储
*/
function getStationBusinessConfig($stationType) {
// ========== 所有工位统一走自动生成路径 ==========
//
// 之前的内置工位配置(inbound, pcb_test, battery, assembly, finished_test,
// delivery, warehouse)已全部移除。
//
// 现在所有工位(包括内置和动态)都通过 FrontController::buildGenericBizConfig()
// 自动生成业务配置,确保与 aging/semi_finished 等已验证正常的工位使用完全相同的代码路径。
//
// 特殊业务逻辑(如 delivery 的箱序列号、inbound 的成品入库切换等)
// 将逐步在 FrontController::handleBuiltinSubmit() 中按 stationType 判断实现。
//
// 如需恢复某个工位的特殊配置,请以 aging/semi_finished 的自动生成配置为基准,
// 在此基础上增量添加,避免重写整个配置导致不一致。
$configs = [
// ========== 成品入库工位(已合并到入库) ==========
// warehouse 重定向到 inbound,保留此逻辑
'warehouse' => [
'model' => 'StationDefinition',
'redirect' => 'inbound',
],
];
if (!isset($configs[$stationType])) {
return null;
}
$bizConfig = $configs[$stationType];
// 动态构建 codePrefix:从 codePrefixFields 映射到实际前缀规则
if (!empty($bizConfig['codePrefixFields'])) {
$bizConfig['codePrefix'] = buildCodePrefixRules($bizConfig['codePrefixFields']);
}
return $bizConfig;
}
/**
* 获取工位的测试状态颜色映射(用于 labelFields 动态颜色)
*/
function getTestStatusColor($status) {
$map = [
'passed' => 'success',
'failed' => 'danger',
'pending' => 'warning',
'通过' => 'success',
'失败' => 'danger',
'待测试' => 'warning',
];
return $map[$status] ?? 'default';
}
File diff suppressed because it is too large Load Diff