初始化
This commit is contained in:
@@ -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,'&').replace(/"/g,'"').replace(/</g,'<').replace(/>/g,'>'); }
|
||||
|
||||
// ===== 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();
|
||||
}
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user