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

1243 lines
59 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
/**
* 工位看板统一模板辅助函数
*
* 将工位操作视图的差异抽象为配置数组,通过统一模板渲染。
* 支持两种工位类型:
* 1. 内置工位:通过 station_business_helper.php 中的 bizConfig 定义
* 2. 动态工位:通过 workstation 表的 fields_config + station_records 表存储
*
* 新添加工位时只需:
* 1. 在 Admin 后台添加 workstation 记录
* 2. 如需特殊业务逻辑,在 station_business_helper.php 添加配置
* 3. 无需创建新视图文件或修改控制器
*/
require_once __DIR__ . '/field_helper.php';
require_once __DIR__ . '/station_business_helper.php';
/**
* 渲染完整的工位操作视图
* @param array $config 工位配置(优先从 bizConfig 读取,否则使用传入的 config)
*
* 配置项说明:
* ============
* 基础信息:
* stationKey string 工位标识(用于 PAGE_KEY、localStorage key 等),必填
* pageTitle string 页面标题文字,必填
* pageIcon string FontAwesome 图标类名(不含 fa- 前缀),必填
* cardTitle string 录入表单卡片标题,默认"快速录入"
* tableTitle string 记录表格卡片标题,默认"操作记录"
*
* 数据与字段:
* dataSources array 数据源 ['types'=>[], 'models'=>[], 'customers'=>[]],默认空
* showOperator bool 是否在表单中显示操作人,默认 true
* excludeFields array 排除的字段名列表(不在表单中渲染),默认空
*
* 提交方式:
* submitMode string 'auto'(扫描字段回车直接提交)| 'button'(需点击按钮),默认 'auto'
* submitBtn array 提交按钮配置 ['text'=>'提交', 'icon'=>'fa-check', 'class'=>'btn-success']
* extraBtns array 额外按钮列表,每个元素: ['id'=>'btnId', 'text'=>'...', 'icon'=>'fa-...', 'class'=>'btn-info']
* 注意:为符合 CSP(无 unsafe-inline),额外按钮不再支持内联 onclick,
* 请在 customInit/customJs 中按 id 绑定事件(如 $('#btnId').on('click', ...))。
* reloadAfterSubmit bool 提交后是否刷新页面,默认 false(只清空扫描字段)
*
* 表格显示:
* codeFields array 需要用 <code> 样式显示的字段名列表,默认 ['serial_no']
* labelFields array 需要用 label 样式显示的字段名列表,格式: ['field_name' => 'label-success|label-warning|label-danger|label-info']
* showRecordBadge bool 是否显示"共 N 条"徽章,默认 true
* emptyText string 空记录提示文字,默认"暂无记录"
*
* 特殊业务:
* customHeader string 额外的卡片头部 HTML(如筛选按钮、导出按钮),默认空
* customBeforeForm string 在 renderStationFields 之前插入的 HTML(如固定产品类型字段),默认空
* customInit string 额外的 $(function(){}) 初始化 JS 代码,默认空
* customJs string 完全自定义的额外 JS 代码(放在 script 标签末尾),默认空
* extraCard string 额外的卡片 HTML(插入在记录表格之后),默认空
*
* 筛选联动(仅 submitMode='auto' 时可用):
* enableFilter bool 是否启用产品类型→型号联动筛选,默认 false
* filterUrl string 筛选记录加载的 URL(相对于 BASE_URL),如 '/Front/inbound'
*
* 内置工位扩展:
* bizConfig array 来自 station_business_helper 的业务配置(自动注入)
*/
function renderStationTemplate($config) {
// ========== 从 GLOBALS 获取业务配置 ==========
$bizConfig = $GLOBALS['bizConfig'] ?? null;
$stationType = $GLOBALS['stationType'] ?? ($config['stationKey'] ?? '');
// 如果有 bizConfig,用它增强基础 config
if ($bizConfig && !empty($bizConfig['view'])) {
$config = array_merge($config, $bizConfig['view']);
}
// ========== 默认值合并 ==========
$defaults = [
'cardTitle' => '快速录入',
'tableTitle' => '操作记录',
'dataSources' => ['types' => [], 'models' => [], 'customers' => []],
'showOperator' => true,
'excludeFields' => [],
'submitMode' => 'auto',
'submitBtn' => ['text' => '确认提交', 'icon' => 'fa-check', 'class' => 'btn-success'],
'extraBtns' => [],
'reloadAfterSubmit'=> false,
'codeFields' => ['serial_no'],
'labelFields' => [],
'showRecordBadge' => true,
'emptyText' => '暂无记录',
'customHeader' => '',
'customBeforeForm' => '',
'customInit' => '',
'customJs' => '',
'extraCard' => '',
'enableFilter' => false,
'filterUrl' => '',
];
// 获取外部变量
$activeStation = $stationType;
$sysConfig = $GLOBALS['sysConfig'] ?? null;
$user = $GLOBALS['user'] ?? [];
$fieldConfigs = $GLOBALS['fieldConfigs'] ?? [];
$records = $GLOBALS['records'] ?? [];
$types = $GLOBALS['types'] ?? [];
$models = $GLOBALS['models'] ?? [];
$customers = $GLOBALS['customers'] ?? [];
$filterType = $GLOBALS['filterType'] ?? '';
$filterModel = $GLOBALS['filterModel'] ?? '';
$todayCount = $GLOBALS['todayCount'] ?? 0;
$totalCount = $GLOBALS['totalCount'] ?? count($records);
$shellColors = $GLOBALS['shellColors'] ?? [];
$stationError = $GLOBALS['stationError'] ?? '';
// 预计算前缀规则(避免在 JS 函数模板中重复创建 SysConfig 对象)
$prefixTypeMap = [];
if ($sysConfig && is_array($sysConfig)) {
try {
$sysConfigModel = new \app\models\SysConfig();
$prefixTypeMap = $sysConfigModel->getCodePrefixRules();
} catch (\Throwable $e) {
error_log('[renderStationTemplate] getCodePrefixRules failed: ' . $e->getMessage());
$prefixTypeMap = [];
}
}
$codePrefixRulesJson = json_encode($prefixTypeMap, JSON_UNESCAPED_UNICODE);
// 前端拼音首字母回退(与后端 SysConfig::pinyinInitials 保持一致),
// 用于动态前缀类型在后台未填前缀时,仍能按类型名拼音首字母校验。
$pinyinMapJson = json_encode([
'外壳' => 'WK', '底壳' => 'WK', '包装' => 'BZ', '半成品' => 'BCP', '半成' => 'BCP',
'配件' => 'PJ', '成品' => 'CP', '电池' => 'DC', '电芯' => 'DC', 'PCBA' => 'XLB',
], JSON_UNESCAPED_UNICODE);
// 前缀 -> 类型 反向映射(用于扫码自动判断产品类型)。键统一为大写前缀
// 注意:SysConfig::getCodePrefixRules() 会对别名(电芯/底壳/半成)做双向自动补全,
// 导致同一前缀可能同时对应主类型和别名(如 dc 同时对应 电池 与 电芯)。
// 自动判类型必须唯一命中主类型,故此处分两遍构建:主类型优先占用前缀,别名仅填补空缺。
$aliasTypes = ['电芯', '底壳', '半成'];
$prefixToTypeMap = [];
foreach ($prefixTypeMap as $type => $prefix) {
if (empty($prefix) || in_array($type, $aliasTypes, true)) continue;
$prefixToTypeMap[strtoupper($prefix)] = $type;
}
foreach ($prefixTypeMap as $type => $prefix) {
if (empty($prefix) || !in_array($type, $aliasTypes, true)) continue;
$k = strtoupper($prefix);
if (!isset($prefixToTypeMap[$k])) {
$prefixToTypeMap[$k] = $type;
}
}
$prefixToTypeJson = json_encode($prefixToTypeMap, JSON_UNESCAPED_UNICODE);
$config = array_merge($defaults, $config);
// 确保数据源正确
$config['dataSources'] = array_merge(
['types' => $types, 'models' => $models, 'customers' => $customers],
$config['dataSources']
);
// 外壳颜色注入到数据源
if (!empty($shellColors)) {
$config['dataSources']['shell_colors'] = $shellColors;
}
$stationKey = $config['stationKey'];
$pageTitle = $config['pageTitle'];
$pageIcon = $config['pageIcon'];
$operatorName = $config['showOperator'] ? ($user['emp_name'] ?? '') : '';
$submitMode = $config['submitMode'];
$enableFilter = $config['enableFilter'];
$filterUrl = $config['filterUrl'];
$recordCount = count($records);
// 构建特殊格式化回调
$specialFormats = [];
foreach ($config['codeFields'] as $fn) {
$specialFormats[$fn] = function($val) { return $val ? '<code>' . htmlspecialchars($val) . '</code>' : '-'; };
}
foreach ($config['labelFields'] as $fn => $cls) {
if ($cls === '__dynamic__') {
// 动态颜色:根据值自动选择
$specialFormats[$fn] = function($val) {
if (!$val) return '-';
$colorMap = ['passed' => 'success', 'failed' => 'danger', 'pending' => 'warning', '通过' => 'success', '失败' => 'danger', '待测试' => 'warning'];
$cls = $colorMap[$val] ?? 'default';
return '<span class="label label-' . $cls . '">' . htmlspecialchars($val) . '</span>';
};
} else {
$specialFormats[$fn] = function($val) use ($cls) {
return $val ? '<span class="label ' . $cls . '">' . htmlspecialchars($val) . '</span>' : '-';
};
}
}
// ========== 输出 HTML(含统一框架) ==========
// 安全检查:防止 station_header.php 被重复包含
static $headerIncluded = false;
if ($headerIncluded) {
// 已输出过框架,记录错误但继续(避免双重框架)
error_log('[renderStationTemplate] WARNING: station_header already included for ' . $stationKey . ' - possible double render detected');
return;
}
$headerIncluded = true;
include APP_PATH . 'app/views/layouts/station_header.php';
?>
<div class="content">
<a href="<?php echo BASE_URL; ?>/Front/index" class="back-link"><i class="fa fa-arrow-left"></i> 返回工位选择</a>
<div class="page-title-bar">
<div>
<h1><i class="fa fa-<?php echo $pageIcon; ?>"></i> <?php echo htmlspecialchars($pageTitle); ?></h1>
</div>
<?php if (!empty($config['customHeader'])): ?>
<?php echo $config['customHeader']; ?>
<?php endif; ?>
<?php if ($todayCount > 0 || $totalCount > 0): ?>
<div class="today-count-badge">
<i class="fa fa-user"></i> <?php echo htmlspecialchars($user['emp_name'] ?? ''); ?>
&nbsp;今日录入:<span class="count-num" id="todayCount"><?php echo $todayCount; ?></span> 条
&nbsp;|&nbsp;总共:<span class="count-num" id="totalCount"><?php echo $totalCount; ?></span> 条
</div>
<?php endif; ?>
</div>
<?php if ($stationError): ?>
<div class="alert alert-danger" style="margin-bottom:15px;">
<i class="fa fa-exclamation-circle"></i> <?php echo htmlspecialchars($stationError); ?>
</div>
<?php endif; ?>
<!-- 录入表单卡片 -->
<div class="card">
<div class="card-header">
<h3><i class="fa fa-plus-circle"></i> <?php echo htmlspecialchars($config['cardTitle']); ?></h3>
<div class="column-adjuster" title="调整表单列数">
<button type="button" class="col-btn" data-cols="1" title="1列"><i class="fa fa-align-justify"></i></button>
<button type="button" class="col-btn" data-cols="2" title="2列"><i class="fa fa-columns"></i>2</button>
<button type="button" class="col-btn active" data-cols="3" title="3列"><i class="fa fa-columns"></i>3</button>
<button type="button" class="col-btn" data-cols="4" title="4列"><i class="fa fa-columns"></i>4</button>
<button type="button" class="col-btn" data-cols="auto" title="自适应"><i class="fa fa-arrows-h"></i> 自动</button>
</div>
</div>
<div class="card-body">
<form method="post" action="" id="mainForm">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($GLOBALS['csrfToken'] ?? ''); ?>">
<input type="hidden" name="action" value="add">
<div class="form-row">
<?php if ($config['customBeforeForm']): ?>
<?php echo $config['customBeforeForm']; ?>
<?php endif; ?>
<?php
// 如果有 bizConfig 且设置了 fixedType,先输出隐藏的 product_type
$actualExcludeFields = $config['excludeFields'] ?? [];
if ($bizConfig && !empty($bizConfig['fixedType'])):
$actualExcludeFields[] = 'product_type'; // 避免与下方的 hidden input 重复
?>
<input type="hidden" name="product_type" value="<?php echo htmlspecialchars($bizConfig['fixedType']); ?>">
<?php endif; ?>
<?php echo renderStationFields($fieldConfigs, $config['dataSources'], $operatorName, $actualExcludeFields); ?>
</div>
<?php
// ===== 入库工位:箱序列号(成品时显示) =====
if ($bizConfig && !empty($bizConfig['extraFields'])):
foreach ($bizConfig['extraFields'] as $efName => $ef):
?>
<div class="row" id="extraField_<?php echo $efName; ?>" style="display:none; margin-top:15px;">
<div class="col-md-4">
<div class="form-group">
<label><?php echo htmlspecialchars($ef['label']); ?> <small class="text-muted">(可选)</small></label>
<input type="text" name="<?php echo htmlspecialchars($efName); ?>"
class="form-control <?php echo !empty($ef['scan']) ? 'scan-submit' : ''; ?>"
placeholder="扫描<?php echo htmlspecialchars($ef['label']); ?>" autocomplete="off">
</div>
</div>
</div>
<?php endforeach; endif; ?>
<?php if ($submitMode === 'button'): ?>
<div class="form-actions">
<button type="button" id="submitBtn" class="btn btn-lg <?php echo $config['submitBtn']['class']; ?>">
<i class="fa <?php echo $config['submitBtn']['icon']; ?>"></i> <?php echo htmlspecialchars($config['submitBtn']['text']); ?>
</button>
<?php foreach ($config['extraBtns'] as $btn): ?>
<button type="button" id="<?php echo $btn['id']; ?>" class="btn btn-lg <?php echo $btn['class'] ?? 'btn-info'; ?>">
<i class="fa <?php echo $btn['icon'] ?? 'fa-cog'; ?>"></i> <?php echo htmlspecialchars($btn['text']); ?>
</button>
<?php endforeach; ?>
</div>
<?php endif; ?>
</form>
</div>
</div>
<!-- 记录表格卡片 -->
<div class="card <?php echo ($enableFilter && ($filterType || $filterModel)) ? 'filter-active' : ''; ?>" id="recordBox">
<div class="card-header">
<h3><i class="fa fa-list"></i>
<?php if ($enableFilter && ($filterType || $filterModel)): ?>
筛选:<strong><?php echo htmlspecialchars($filterType ?: '全部类型'); ?></strong> /
<strong><?php echo htmlspecialchars($filterModel ?: '全部型号'); ?></strong>
<?php else: ?>
<?php echo htmlspecialchars($config['tableTitle']); ?>
<?php endif; ?>
</h3>
<div style="display:flex;align-items:center;gap:10px;flex-wrap:wrap;">
<?php if ($config['showRecordBadge']): ?>
<span class="badge">
<?php if ($enableFilter && ($filterType || $filterModel)): ?>
筛选结果 <?php echo $recordCount; ?> 条 | 总计 <?php echo $totalCount; ?> 条
<?php else: ?>
共 <?php echo $recordCount; ?> 条<?php if ($totalCount > 0 && $totalCount != $recordCount): ?> | 总计 <?php echo $totalCount; ?> 条<?php endif; ?>
<?php endif; ?>
</span>
<?php endif; ?>
<?php if ($enableFilter && ($filterType || $filterModel)): ?>
<button type="button" id="clearFilterBtn" class="btn btn-sm btn-warning"><i class="fa fa-times"></i> 清除</button>
<?php endif; ?>
<?php echo renderTableColumnSelector($stationKey, $fieldConfigs); ?>
</div>
</div>
<div class="card-body no-padding">
<div class="table-wrap">
<table class="table">
<thead>
<tr><?php echo renderRecordTableHeader($fieldConfigs); ?></tr>
</thead>
<tbody>
<?php if (!empty($records)): ?>
<?php foreach ($records as $record): ?>
<?php echo renderRecordTableRow($record, $fieldConfigs, $specialFormats); ?>
<?php endforeach; ?>
<?php else: ?>
<tr>
<td colspan="20">
<div class="empty-state">
<?php if ($enableFilter && !($filterType || $filterModel)): ?>
<i class="fa fa-hand-pointer-o"></i><p>请先选择产品型号,系统将自动加载对应记录</p>
<?php else: ?>
<i class="fa fa-inbox"></i><p><?php echo htmlspecialchars($config['emptyText']); ?></p>
<?php endif; ?>
</div>
</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
<?php if ($config['extraCard']): ?>
<?php echo $config['extraCard']; ?>
<?php endif; ?>
</div>
<script nonce="<?php echo csp_nonce(); ?>">
var PAGE_KEY = '<?php echo $stationKey; ?>';
var BASE_URL = '<?php echo BASE_URL; ?>';
var STATION_TYPE = '<?php echo $stationType; ?>';
// ========== 表单列数调节 ==========
(function() {
var $grid = $('#stationFieldsGrid');
if (!$grid.length) return;
var COL_KEY = 'station_form_cols_' + STATION_TYPE;
function applyColumns(cols) {
$grid.attr('data-columns', cols);
$('.column-adjuster .col-btn').removeClass('active');
$('.column-adjuster .col-btn[data-cols="' + cols + '"]').addClass('active');
try { localStorage.setItem(COL_KEY, cols); } catch(e) {}
}
var savedCols = localStorage.getItem(COL_KEY) || '3';
applyColumns(savedCols);
$('.column-adjuster .col-btn').click(function() {
applyColumns($(this).data('cols'));
});
})();
// ========== 筛选联动(仅 enableFilter 模式) ==========
<?php if ($enableFilter): ?>
var modelsCache = {};
function loadModelsByType(productType, callback) {
if (!productType) { callback([]); return; }
if (modelsCache[productType]) { callback(modelsCache[productType]); return; }
$.ajax({
url: BASE_URL + '/Front/getModelsByType',
type: 'GET', data: {product_type: productType}, dataType: 'json',
success: function(res) {
if (res.success) {
modelsCache[productType] = res.models;
callback(res.models);
} else {
console.error('getModelsByType 返回失败:', res);
callback([]);
}
},
error: function(xhr, status, error) {
console.error('联动请求失败', status, error);
// 不调用 callback,保持型号选择框现有状态不变
}
});
}
function updateModelSelect(models) {
var $sel = $('select[name="product_model"]');
if (!$sel.length) return;
var currentVal = $sel.val();
// 如果返回空列表,保留当前选项不清空,避免型号选择框"消失"
if (!models || models.length === 0) {
// 只在完全没有 option 时才添加占位提示
if ($sel.find('option').length === 0) {
$sel.append('<option value="">暂无型号数据</option>');
}
return;
}
$sel.empty().append('<option value="">请选择产品型号</option>');
$.each(models, function(i, m) {
var selected = (m.model_code == currentVal) ? ' selected' : '';
$sel.append('<option value="' + $('<div/>').text(m.model_code).html() + '"' + selected + '>' + $('<div/>').text(m.model_code).html() + '</option>');
});
}
function loadRecords(productType, productModel) {
if (!productType && !productModel) return;
window.location.href = BASE_URL + '<?php echo $filterUrl; ?>?product_type=' + encodeURIComponent(productType) + '&product_model=' + encodeURIComponent(productModel);
}
function clearFilter() { try { sessionStorage.removeItem('filter_' + PAGE_KEY); } catch(e) {} window.location.href = BASE_URL + '<?php echo $filterUrl; ?>'; }
$(document).on('change', 'select[name="product_type"]', function() {
var pt = $(this).val();
if (pt) {
loadModelsByType(pt, updateModelSelect);
} else {
// 清空 product_type 时也清空型号列表(用户主动清空筛选)
updateModelSelect([]);
}
<?php if ($bizConfig && !empty($bizConfig['extraFields'])): ?>
// 入库工位:选"成品"时显示箱序列号
<?php foreach ($bizConfig['extraFields'] as $efName => $ef): ?>
<?php if (!empty($ef['showWhen']) && $ef['showWhen']['field'] === 'product_type'): ?>
if (pt === '<?php echo $ef['showWhen']['value']; ?>') {
$('#extraField_<?php echo $efName; ?>').show();
// 更新序列号标签
$('label').filter(function() { return $(this).text().trim().indexOf('产品序列号') >= 0; }).text('成品序列号 * (回车添加)');
} else {
$('#extraField_<?php echo $efName; ?>').hide();
$('label').filter(function() { return $(this).text().trim().indexOf('成品序列号') >= 0; }).text('产品序列号 * (回车添加)');
}
<?php endif; ?>
<?php endforeach; ?>
<?php endif; ?>
});
$(document).on('change', 'select[name="product_model"]', function() {
// 兼容 select 和 hidden input 两种 product_type
var pt = ($('select[name="product_type"]').val() || $('input[name="product_type"]').val() || '').trim();
var pm = $(this).val();
if (pm) {
// ★ 保存筛选参数到 sessionStorage,确保页面重载后能恢复选中状态
try { sessionStorage.setItem('filter_' + PAGE_KEY, JSON.stringify({pt: pt, pm: pm})); } catch(e) {}
loadRecords(pt, pm);
}
});
<?php endif; ?>
// ========== 型号加载(固定类型工位:pcb_test, battery ==========
<?php if ($bizConfig && !empty($bizConfig['fixedType']) && !$enableFilter): ?>
var modelsCache2 = {};
function loadModelsByType2(productType, callback) {
if (!productType) { callback([]); return; }
if (modelsCache2[productType]) { callback(modelsCache2[productType]); return; }
$.ajax({
url: BASE_URL + '/Front/getModelsByType', type: 'GET', data: {product_type: productType}, dataType: 'json',
success: function(res) {
if (res.success) {
modelsCache2[productType] = res.models;
callback(res.models);
} else {
console.error('getModelsByType 返回失败:', res);
callback([]);
}
},
error: function(xhr, status, error) {
console.error('联动请求失败', status, error);
// 不调用 callback,保持型号选择框现有状态不变
}
});
}
function updateModelSelect2(models, stickyProductModel) {
var $sel = $('select[name="product_model"]');
if (!$sel.length) return;
var cv = (stickyProductModel !== undefined) ? stickyProductModel : $sel.val();
// 如果返回空列表,保留当前选项不清空,避免型号选择框"消失"
if (!models || models.length === 0) {
if ($sel.find('option').length === 0) {
$sel.append('<option value="">暂无型号数据</option>');
}
return;
}
$sel.empty().append('<option value="">请选择产品型号</option>');
$.each(models, function(i, m) {
$sel.append('<option value="' + $('<div/>').text(m.model_code).html() + '"' + (m.model_code == cv ? ' selected' : '') + '>' + $('<div/>').text(m.model_code).html() + '</option>');
});
}
<?php endif; ?>
// ========== 序列号自动查找型号(delivery 工位) ==========
<?php if ($bizConfig && !empty($bizConfig['serialLookup'])): ?>
var lookupTimer = null;
$(document).on('input', 'input[name="finished_serial"]', function() {
clearTimeout(lookupTimer);
var val = $(this).val().trim();
var $modelDisplay = $('#modelLookupDisplay');
if (!$modelDisplay.length) {
$modelDisplay = $('<div id="modelLookupDisplay" class="model-display" style="display:none;margin-top:8px;">'
+ '<label>识别型号:</label>'
+ '<span id="modelDisplayText" style="font-weight:bold;color:var(--primary);"></span>'
+ '</div>');
$(this).closest('.station-field-item, .form-group, .col-md-4, .col-md-').after($modelDisplay);
}
$modelDisplay.hide();
$('input[name="product_model"]').val('');
if (val.length < 2) return;
lookupTimer = setTimeout(function() {
$.ajax({
url: window.location.href, type: 'POST',
data: {
action: 'lookup', finished_serial: val,
csrf_token: $('input[name="csrf_token"]').val()
},
dataType: 'json',
success: function(res) {
if (res.success) {
if (res.type === 'box') {
var summaryText = [];
for (var pm in res.summary) {
summaryText.push(pm + ' ×' + res.summary[pm].count);
}
$('#modelDisplayText').html(
'<span style="color:#e67e22;">📦 箱序列号</span> 共' + res.total + '件:'
+ summaryText.join('')
);
$('input[name="product_model"]').val('BOX:' + res.box_serial);
} else {
$('#modelDisplayText').text(res.product_model);
$('input[name="product_model"]').val(res.product_model);
}
$modelDisplay.show();
} else {
$('#modelDisplayText').text('未找到,请确认序列号');
$modelDisplay.show();
}
}
});
}, 500);
});
<?php endif; ?>
// ========== 提示音播放 ==========
function playErrorSound() {
try {
// 使用 Web Audio API 生成蜂鸣提示音
var AudioContext = window.AudioContext || window.webkitAudioContext;
if (!AudioContext) return;
var ctx = new AudioContext();
var osc = ctx.createOscillator();
var gain = ctx.createGain();
osc.type = 'square';
osc.frequency.setValueAtTime(800, ctx.currentTime);
osc.frequency.setValueAtTime(600, ctx.currentTime + 0.15);
gain.gain.setValueAtTime(0.3, ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.4);
osc.connect(gain);
gain.connect(ctx.destination);
osc.start(ctx.currentTime);
osc.stop(ctx.currentTime + 0.4);
} catch(e) {
// 降级:忽略音频错误
}
}
// ========== 弹窗提示 ==========
function showErrorDialog(message) {
// 播放提示音
playErrorSound();
// 使用浏览器原生 alert 弹窗(确保用户看到)
alert(message);
}
// ========== 错误高亮工具函数 ==========
function highlightError($input, message, showAlert) {
// 高亮输入框
$input.addClass('input-error');
$input.focus().select();
// 移除旧提示
$input.closest('.station-field-item, .form-group').find('.field-error-tip').remove();
// 添加错误提示
$input.after('<span class="field-error-tip" style="color:#d9534f;font-size:12px;display:block;margin-top:4px;">' + message + '</span>');
// 弹窗 + 提示音
if (showAlert !== false) {
playErrorSound();
// 延迟弹窗,确保 DOM 先更新
setTimeout(function() {
alert(message);
}, 50);
}
// 3秒后自动清除高亮(但保留提示文字)
setTimeout(function() {
$input.removeClass('input-error');
$input.closest('.station-field-item, .form-group').find('.field-error-tip').fadeOut(300, function() { $(this).remove(); });
}, 5000);
// 输入时清除错误状态
$input.one('input change', function() {
$input.removeClass('input-error');
$input.closest('.station-field-item, .form-group').find('.field-error-tip').remove();
});
}
// ========== 实时前缀检查(失焦时静默提示,不弹窗) ==========
// 在扫描字段失焦时检查前缀是否匹配,如果不符合则显示内联提示但不阻止操作
function checkPrefixOnBlur($input) {
var fieldName = $input.attr('name');
if (!fieldName) return;
var prefixRules = <?php echo json_encode($bizConfig['codePrefix'] ?? [], JSON_UNESCAPED_UNICODE); ?>;
var prefixTypeMap = <?php echo $codePrefixRulesJson; ?>;
if (prefixRules[fieldName]) {
var rule = prefixRules[fieldName];
var prefix = rule[0];
var msg = rule[1] || (fieldName + ' 前缀错误');
var val = $input.val().trim();
if (val === '') return;
// 清除旧提示
$input.closest('.station-field-item, .form-group').find('.prefix-hint').remove();
$input.removeClass('input-warning');
// 大小写不敏感匹配,适配用户输入 DC/dc/Dc 等字符写法
var upVal = val.toUpperCase();
if (prefix === '*dynamic*') {
// 统一解析产品类型:select / hidden input / 固定类型兜底;装配页按字段名解析固定类型
var productType = getCurrentProductType();
if (!productType && fieldToAssemblyType[fieldName]) {
productType = fieldToAssemblyType[fieldName];
}
// 后台未配前缀时回退为类型名拼音首字母,落实所有工位受前缀管控
var expectedPrefix = productType ? (prefixTypeMap[productType] || pinyinInitials(productType) || '') : '';
if (expectedPrefix) {
var upExp = expectedPrefix.toUpperCase();
if (upVal.indexOf(upExp) !== 0) {
$input.addClass('input-warning');
var m1 = productType + '序列号应以「' + expectedPrefix + '」开头';
$input.after('<span class="prefix-hint" style="color:#e67e22;font-size:12px;display:block;margin-top:4px;">⚠ ' + m1 + '</span>');
// 失焦静默提示,不弹窗、不重新聚焦(避免反复弹窗导致关不掉);实际拦截在提交时
} else {
// 前缀正确,显示绿色提示
$input.addClass('input-valid');
$input.after('<span class="prefix-hint" style="color:#27ae60;font-size:12px;display:block;margin-top:4px;">✓ 前缀「' + expectedPrefix + '」正确</span>');
}
} else {
// 原则:所有工位序列号都受前缀管控,未配置前缀时给出明确警示(仅内联提示,不弹窗)
$input.addClass('input-warning');
var m2 = productType
? ('产品类型「' + productType + '」未配置序列号前缀,请联系管理员设置')
: '该字段未关联产品类型,无法校验序列号前缀,请联系管理员';
$input.after('<span class="prefix-hint" style="color:#e67e22;font-size:12px;display:block;margin-top:4px;">⚠ ' + m2 + '</span>');
}
} else {
var upPrefix = prefix.toUpperCase();
if (upVal.indexOf(upPrefix) !== 0) {
$input.addClass('input-warning');
$input.after('<span class="prefix-hint" style="color:#e67e22;font-size:12px;display:block;margin-top:4px;">⚠ ' + msg + '</span>');
// 失焦静默提示,不弹窗、不重新聚焦(避免反复弹窗导致关不掉)
} else {
$input.addClass('input-valid');
$input.after('<span class="prefix-hint" style="color:#27ae60;font-size:12px;display:block;margin-top:4px;">✓ 前缀「' + prefix + '」正确</span>');
}
}
// 3秒后自动清除提示
setTimeout(function() {
$input.removeClass('input-warning input-valid');
$input.closest('.station-field-item, .form-group').find('.prefix-hint').fadeOut(300, function() { $(this).remove(); });
}, 3000);
}
}
// 前端拼音首字母映射(类型名 -> 拼音首字母),用于动态前缀回退
var pinyinInitialsMap = <?php echo $pinyinMapJson; ?>;
function pinyinInitials(type) {
if (!type) return '';
if (pinyinInitialsMap[type] !== undefined) return pinyinInitialsMap[type];
// 含英文的类型(如 PCBA):取前两位字母大写
var m = type.match(/[A-Za-z0-9]+/g);
if (m) return m.join('').substring(0, 2).toUpperCase();
// 中文:首字(简化——生僻字可能不准,主要类型已映射)
return type.substring(0, 1);
}
// ========== 扫码自动判断产品类型(按前缀反查) ==========
// 扫描/输入序列号后,根据前缀自动选中 product_type 下拉并联动加载型号
var prefixToTypeMap = <?php echo $prefixToTypeJson; ?>;
// 固定产品类型(hidden input 工位或固定工位),无 product_type 下拉时兜底使用
var FIXED_TYPE = <?php echo json_encode($bizConfig['fixedType'] ?? ''); ?>;
// 装配页:字段名 -> 固定类型(逆向 assemblyTypeToField),用于无 product_type 时按字段解析前缀
var fieldToAssemblyType = { 'finished_serial': '成品', 'battery_serial': '电池', 'pcb_serial': 'PCBA' };
// 统一获取当前 product_type:兼容 select / hidden input / 固定类型兜底(避免取到 undefined)
function getCurrentProductType() {
var sel = $('select[name="product_type"]').val();
if (sel) return sel;
var hid = $('input[name="product_type"]').val();
if (hid) return hid;
if (FIXED_TYPE) return FIXED_TYPE;
return '';
}
// 根据序列号前缀反查产品类型(优先匹配更长前缀)
function detectTypeByPrefix(val) {
if (!val) return '';
var up = val.toUpperCase();
var best = '';
for (var p in prefixToTypeMap) {
if (!prefixToTypeMap.hasOwnProperty(p)) continue;
if (up.indexOf(p) === 0 && (best === '' || p.length > best.length)) {
best = prefixToTypeMap[p];
}
}
return best;
}
// 自动选中产品类型下拉(仅当存在可见的 product_type 下拉时生效)
function autoSelectTypeBySerial($input) {
if (!$input || !$input.length) return;
var $sel = $('select[name="product_type"]');
if ($sel.length === 0) return;
var serial = ($input.val() ? $input.val().trim() : '');
if (serial === '') return;
var detected = detectTypeByPrefix(serial);
if (detected === '' || $sel.val() === detected) return;
$sel.val(detected).trigger('change');
}
// ========== 装配页:扫描序列号按前缀自动归到对应字段 ==========
// 装配页为固定三栏(成品/电池/PCB),无 product_type 下拉,
// 因此"自动判类型"表现为:扫到序列号后按前缀自动填入对应的字段。
var assemblyTypeToField = {
'成品': 'finished_serial',
'电池': 'battery_serial',
'PCBA': 'pcb_serial'
};
function assemblyAutoRoute($input) {
if (!$input || !$input.length) return;
// 仅当页面存在成品/电池/PCB 三栏时启用(装配页特征)
if ($('[name="finished_serial"]').length === 0) return;
var serial = ($input.val() ? $input.val().trim() : '');
if (serial === '') return;
var detected = detectTypeByPrefix(serial);
if (detected === '' || !assemblyTypeToField[detected]) return;
var targetName = assemblyTypeToField[detected];
if ($input.attr('name') === targetName) return; // 已在正确字段
var $target = $('[name="' + targetName + '"]');
if ($target.length === 0) return;
if ($target.val().trim() !== '') return; // 目标字段已占用,不覆盖
// 将序列号移动到正确字段
$target.val(serial);
$input.val('');
$target.closest('.station-field-item, .form-group').find('.prefix-hint').remove();
checkPrefixOnBlur($target);
$target.after('<span class="prefix-hint" style="color:#2980b9;font-size:12px;display:block;margin-top:4px;">↪ 已按前缀自动归入「' + detected + '」</span>');
setTimeout(function() {
$target.closest('.station-field-item, .form-group').find('.prefix-hint').fadeOut(300, function() { $(this).remove(); });
}, 3000);
}
// ========== 主逻辑 ==========
$(function() {
// --- 恢复 sticky 表单数据 ---
var saved = localStorage.getItem('form_sticky_' + PAGE_KEY);
var sticky = saved ? JSON.parse(saved) : null;
<?php if ($enableFilter): ?>
// CSP:清除筛选按钮改为脚本绑定(替代内联 onclick="clearFilter()"
$('#clearFilterBtn').on('click', clearFilter);
var $typeSel = $('select[name="product_type"]');
var $typeInput = $('input[name="product_type"]');
var $modelSel = $('select[name="product_model"]');
var urlParams = new URLSearchParams(window.location.search);
var urlProductType = urlParams.get('product_type') || '';
var urlProductModel = urlParams.get('product_model') || '';
// 辅助函数:获取当前 product_type 值(兼容 select 和 hidden input
function getProductType() {
if ($typeSel.length) return $typeSel.val() || '';
if ($typeInput.length) return $typeInput.val() || '';
<?php if ($bizConfig && !empty($bizConfig['fixedType'])): ?>
return '<?php echo $bizConfig['fixedType']; ?>';
<?php else: ?>
return '';
<?php endif; ?>
}
// 修复 URL 中 product_type=undefined 的问题
if (urlProductType === 'undefined') urlProductType = getProductType() || '';
if (sticky) {
if (sticky['product_type']) {
if ($typeSel.length) $typeSel.val(sticky['product_type']);
}
var stickyType = sticky['product_type'] || getProductType();
if (stickyType) {
loadModelsByType(stickyType, function(models) {
updateModelSelect(models);
if (sticky['product_model']) $modelSel.val(sticky['product_model']);
Object.keys(sticky).forEach(function(name) {
if (name === 'product_type' || name === 'product_model') return;
var $el = $('[name="' + name + '"]');
if ($el.length && sticky[name] !== null && sticky[name] !== undefined) $el.val(sticky[name]);
});
});
}
localStorage.removeItem('form_sticky_' + PAGE_KEY);
} else if (urlProductType || urlProductModel) {
// URL 参数恢复:如果 product_type 为空但有 product_model,从页面 hidden/select 获取类型
var effectiveType = urlProductType || getProductType();
<?php if ($bizConfig && !empty($bizConfig['fixedType'])): ?>
if (!effectiveType) effectiveType = '<?php echo $bizConfig['fixedType']; ?>';
<?php endif; ?>
if ($typeSel.length) $typeSel.val(effectiveType);
if (effectiveType) {
loadModelsByType(effectiveType, function(models) {
updateModelSelect(models);
if (urlProductModel) $modelSel.val(urlProductModel);
});
} else if (urlProductModel) {
// ★ 修复:即使 product_type 为空,也尝试从 sessionStorage 恢复并直接设置型号选中
// 这解决了筛选返回 0 条记录后产品型号选择框丢失选中状态的问题
var filterData = null;
try { var raw = sessionStorage.getItem('filter_' + PAGE_KEY); if (raw) filterData = JSON.parse(raw); } catch(e) {}
if (filterData && filterData.pm === urlProductModel) {
loadModelsByType(filterData.pt, function(models) {
updateModelSelect(models);
$modelSel.val(urlProductModel);
});
}
}
} else {
// ★ 无 URL 参数时,从 sessionStorage 恢复筛选状态
var filterData = null;
try { var raw = sessionStorage.getItem('filter_' + PAGE_KEY); if (raw) filterData = JSON.parse(raw); } catch(e) {}
if (filterData && filterData.pt) {
if ($typeSel.length) $typeSel.val(filterData.pt);
loadModelsByType(filterData.pt, function(models) {
updateModelSelect(models);
if (filterData.pm) $modelSel.val(filterData.pm);
});
}
var defaultType = getProductType();
<?php if ($bizConfig && !empty($bizConfig['defaultType'])): ?>
defaultType = '<?php echo $bizConfig['defaultType']; ?>';
<?php endif; ?>
if (defaultType && $typeSel.length && $typeSel.val() === '') {
$typeSel.val(defaultType).trigger('change');
}
}
<?php elseif ($bizConfig && !empty($bizConfig['fixedType'])): ?>
// 固定产品类型工位:自动加载型号
var saved2 = localStorage.getItem('form_sticky_' + PAGE_KEY);
var sticky2 = saved2 ? JSON.parse(saved2) : null;
if (saved2) localStorage.removeItem('form_sticky_' + PAGE_KEY);
// 恢复非 select 字段
if (sticky2) {
Object.keys(sticky2).forEach(function(name) {
var $el = $('[name="' + name + '"]');
if ($el.length && name !== 'product_model' && sticky2[name] !== null && sticky2[name] !== undefined) {
$el.val(sticky2[name]);
}
});
}
// 加载固定类型的型号
loadModelsByType2('<?php echo $bizConfig['fixedType']; ?>', function(models) {
updateModelSelect2(models, sticky2 ? sticky2['product_model'] : undefined);
if (sticky2) {
Object.keys(sticky2).forEach(function(name) {
if (name === 'product_model') return;
var $el = $('[name="' + name + '"]');
if ($el.length && $el.is('select') && sticky2[name] !== null && sticky2[name] !== undefined) {
$el.val(sticky2[name]);
}
});
}
});
<?php else: ?>
if (sticky) {
Object.keys(sticky).forEach(function(name) {
var $el = $('[name="' + name + '"]');
if ($el.length && sticky[name] !== null && sticky[name] !== undefined) $el.val(sticky[name]);
});
localStorage.removeItem('form_sticky_' + PAGE_KEY);
}
<?php endif; ?>
// --- 扫描字段是否全填满 ---
function allScanFilled() {
var ok = true;
$('#mainForm .scan-submit:visible').each(function() { if ($(this).val().trim() === '') ok = false; });
return ok;
}
<?php if ($submitMode === 'button'): ?>
// --- 按钮模式:检查所有必填字段 ---
function allRequiredFilled() {
var ok = allScanFilled();
$('#mainForm select[required]:not(.scan-submit), #mainForm input[required]:not(.scan-submit):not([type="hidden"])').each(function() {
if ($(this).val().trim() === '') ok = false;
});
return ok;
}
<?php endif; ?>
// --- 扫描字段失焦时实时前缀检查 ---
$('#mainForm').on('blur', '.scan-submit', function() {
autoSelectTypeBySerial($(this));
assemblyAutoRoute($(this));
checkPrefixOnBlur($(this));
});
// --- 扫描字段回车连环跳转 ---
$('#mainForm').on('keypress', '.scan-submit', function(e) {
if (e.which !== 13) return;
e.preventDefault();
if ($(this).val().trim() === '') return;
// 扫码回车前,先按前缀自动判断产品类型
autoSelectTypeBySerial($(this));
assemblyAutoRoute($(this));
<?php
// ===== 入库工位特殊回车逻辑:序列号→箱序列号 =====
if ($bizConfig && !empty($bizConfig['extraFields'])):
?>
var isSerialNo = ($(this).attr('name') === 'serial_no');
<?php foreach ($bizConfig['extraFields'] as $efName => $ef): ?>
<?php if (!empty($ef['showWhen']) && $ef['showWhen']['field'] === 'product_type'): ?>
var $extra_<?php echo $efName; ?> = $('input[name="<?php echo $efName; ?>"]');
var isFixedType = ($('input[name="product_type"]').val() === '<?php echo $ef['showWhen']['value']; ?>');
if (isSerialNo && isFixedType && $extra_<?php echo $efName; ?>.is(':visible') && $extra_<?php echo $efName; ?>.val().trim() === '') {
$extra_<?php echo $efName; ?>.focus().select();
return;
}
<?php endif; ?>
<?php endforeach; ?>
<?php endif; ?>
<?php if ($submitMode === 'auto'): ?>
if (allScanFilled()) { $('#mainForm').submit(); return; }
<?php else: ?>
// 按钮模式:额外检查必填字段
if (allScanFilled()) {
var $missing = null;
$('#mainForm select[required]:not(.scan-submit), #mainForm input[required]:not(.scan-submit):not([type="hidden"])').each(function() {
if ($(this).val().trim() === '' && !$missing) $missing = $(this);
});
if ($missing) { $missing.focus(); if ($missing.is('select')) $missing[0].dispatchEvent(new Event('mousedown')); return; }
$('#submitBtn').click();
return;
}
<?php endif; ?>
var sf = [];
$('#mainForm .scan-submit:visible').each(function() { sf.push(this); });
var ci = sf.indexOf(this);
for (var i = 1; i < sf.length; i++) {
var idx = (ci + i) % sf.length;
if ($(sf[idx]).val().trim() === '') { $(sf[idx]).focus().select(); return; }
}
<?php if ($submitMode === 'auto'): ?>
$('#mainForm').submit();
<?php else: ?>
$('#submitBtn').click();
<?php endif; ?>
});
<?php if ($submitMode === 'auto'): ?>
// --- 自动模式:表单提交 ---
$('#mainForm').on('submit', function(e) {
e.preventDefault();
var $form = $(this);
// ===== 必填字段前端验证 =====
var $firstError = null;
var firstErrorMsg = '';
// 检查所有带有 required 属性的可见输入框(排除 hidden 和 disabled
$form.find('select[required]:visible:not(:disabled), input[required]:visible:not(:disabled):not([type="hidden"]), textarea[required]:visible:not(:disabled)').each(function() {
var val = $(this).val();
// select 的空值是空字符串,input 需要 trim
if (val === null || val === undefined || (typeof val === 'string' && val.trim() === '')) {
if (!$firstError) {
$firstError = $(this);
var label = $(this).closest('.station-field-item, .form-group').find('label').first().text().replace(/\*/g,'').trim();
firstErrorMsg = '请填写必填项:' + (label || $(this).attr('name') || '未知字段');
}
}
});
if ($firstError) {
highlightError($firstError, firstErrorMsg, true);
return;
}
// 代码前缀前端验证
var prefixRules = <?php echo json_encode($bizConfig['codePrefix'] ?? [], JSON_UNESCAPED_UNICODE); ?>;
var prefixTypeMap = <?php echo $codePrefixRulesJson; ?>;
for (var fieldName in prefixRules) {
var rule = prefixRules[fieldName];
var prefix = rule[0];
var msg = rule[1] || (fieldName + ' 前缀错误');
var $input = $form.find('[name="' + fieldName + '"]');
var val = $input.val().trim();
if (val === '') continue;
// 动态前缀:根据当前 product_type 获取前缀(大小写不敏感,且未配置前缀必须阻止提交)
if (prefix === '*dynamic*') {
var productType = getCurrentProductType();
if (!productType && fieldToAssemblyType[fieldName]) {
productType = fieldToAssemblyType[fieldName];
}
// 后台未配前缀时回退为类型名拼音首字母,落实所有工位受前缀管控
var expectedPrefix = productType ? (prefixTypeMap[productType] || pinyinInitials(productType) || '') : '';
if (expectedPrefix) {
if (val.toUpperCase().indexOf(expectedPrefix.toUpperCase()) !== 0) {
highlightError($input, productType + '序列号必须以 ' + expectedPrefix + ' 开头');
return;
}
} else {
highlightError($input, productType
? ('产品类型「' + productType + '」未配置序列号前缀,请联系管理员设置后再操作')
: '该字段未关联产品类型,无法校验序列号前缀,请联系管理员');
return;
}
} else if (val.toUpperCase().indexOf(prefix.toUpperCase()) !== 0) {
highlightError($input, msg);
return;
}
}
var sticky = {};
$form.find('.sticky-save').each(function() { sticky[$(this).attr('name')] = $(this).val(); });
localStorage.setItem('form_sticky_' + PAGE_KEY, JSON.stringify(sticky));
$.ajax({
url: window.location.href, type: 'POST', data: $form.serialize(), dataType: 'json',
success: function(res) {
if (res.success) {
<?php if ($enableFilter): ?>
// 兼容 select 和 hidden input 两种 product_type
var pt = ($form.find('select[name="product_type"]').val() || $form.find('input[name="product_type"]').val() || '').trim();
var pm = $form.find('select[name="product_model"]').val();
if (pm) {
try { sessionStorage.setItem('filter_' + PAGE_KEY, JSON.stringify({pt: pt, pm: pm})); } catch(e) {}
loadRecords(pt, pm);
} else location.reload();
<?php elseif ($config['reloadAfterSubmit']): ?>
location.reload();
<?php else: ?>
location.reload();
<?php endif; ?>
$form.find('.scan-submit').val('');
var $firstScan = $form.find('.scan-submit').first();
if ($firstScan.length) setTimeout(function() { $firstScan.focus(); }, 200);
} else {
// 处理后端返回的 codePrefix / duplicate 错误
// 所有错误统一收集后弹一个窗,避免多字段连续弹多个 alert
var errMap = res.codePrefixErrors || res.errors || res.duplicateErrors || null;
if (errMap) {
var msgs = [];
var $firstErr = null;
for (var fieldName in errMap) {
var $input = $form.find('[name="' + fieldName + '"]');
// 高亮字段但不各自弹窗(showAlert=false
highlightError($input, errMap[fieldName], false);
if (!$firstErr && $input.length) $firstErr = $input;
msgs.push(errMap[fieldName]);
}
if ($firstErr) $firstErr.focus().select();
showErrorDialog(msgs.join('\n'));
} else {
showErrorDialog('提交失败: ' + (res.error || '未知错误'));
}
}
},
error: function() {
showErrorDialog('网络错误,请重试');
}
});
});
<?php else: ?>
// --- 按钮模式:点击提交 ---
$('#submitBtn').click(function() {
var $form = $('#mainForm');
// ===== 必填字段前端验证 =====
var $firstError = null;
var firstErrorMsg = '';
$form.find('select[required]:visible:not(:disabled), input[required]:visible:not(:disabled):not([type="hidden"]), textarea[required]:visible:not(:disabled)').each(function() {
var val = $(this).val();
if (val === null || val === undefined || (typeof val === 'string' && val.trim() === '')) {
if (!$firstError) {
$firstError = $(this);
var label = $(this).closest('.station-field-item, .form-group').find('label').first().text().replace(/\*/g,'').trim();
firstErrorMsg = '请填写必填项:' + (label || $(this).attr('name') || '未知字段');
}
}
});
if ($firstError) {
highlightError($firstError, firstErrorMsg, true);
return;
}
// 代码前缀前端验证
var prefixRules = <?php echo json_encode($bizConfig['codePrefix'] ?? [], JSON_UNESCAPED_UNICODE); ?>;
var prefixTypeMap = <?php echo $codePrefixRulesJson; ?>;
for (var fieldName in prefixRules) {
var rule = prefixRules[fieldName];
var prefix = rule[0];
var msg = rule[1] || (fieldName + ' 前缀错误');
var $input = $form.find('[name="' + fieldName + '"]');
var val = $input.val().trim();
if (val === '') continue;
// 动态前缀:根据当前 product_type 获取前缀(大小写不敏感,且未配置前缀必须阻止提交)
if (prefix === '*dynamic*') {
var productType = getCurrentProductType();
if (!productType && fieldToAssemblyType[fieldName]) {
productType = fieldToAssemblyType[fieldName];
}
// 后台未配前缀时回退为类型名拼音首字母,落实所有工位受前缀管控
var expectedPrefix = productType ? (prefixTypeMap[productType] || pinyinInitials(productType) || '') : '';
if (expectedPrefix) {
if (val.toUpperCase().indexOf(expectedPrefix.toUpperCase()) !== 0) {
highlightError($input, productType + '序列号必须以 ' + expectedPrefix + ' 开头');
return;
}
} else {
highlightError($input, productType
? ('产品类型「' + productType + '」未配置序列号前缀,请联系管理员设置后再操作')
: '该字段未关联产品类型,无法校验序列号前缀,请联系管理员');
return;
}
} else if (val.toUpperCase().indexOf(prefix.toUpperCase()) !== 0) {
highlightError($input, msg);
return;
}
}
<?php if ($bizConfig && !empty($bizConfig['serialLookup'])): ?>
var serial = $form.find('input[name="finished_serial"]').val().trim();
if (!serial) { showErrorDialog('请扫描序列号'); return; }
if (!$form.find('select[name="customer_name"]').val()) { showErrorDialog('请选择客户'); return; }
<?php endif; ?>
var sticky = {};
$form.find('.sticky-save').each(function() { sticky[$(this).attr('name')] = $(this).val(); });
localStorage.setItem('form_sticky_' + PAGE_KEY, JSON.stringify(sticky));
$.ajax({
url: window.location.href, type: 'POST', data: $form.serialize(), dataType: 'json',
success: function(res) {
if (res.success) {
<?php if ($bizConfig && !empty($bizConfig['serialLookup'])): ?>
if (res.type === 'box') {
alert(res.message || ('箱序列号导入成功,共' + res.imported + '件'));
}
<?php endif; ?>
$('#mainForm .scan-submit').val('');
$('#mainForm .scan-submit').first().focus();
<?php if ($config['reloadAfterSubmit']): ?>
location.reload();
<?php else: ?>
location.reload();
<?php endif; ?>
} else {
// 处理后端返回的 codePrefix / duplicate 错误
// 所有错误统一收集后弹一个窗,避免多字段连续弹多个 alert
var errMap = res.codePrefixErrors || res.errors || res.duplicateErrors || null;
if (errMap) {
var msgs = [];
var $firstErr = null;
for (var fieldName in errMap) {
var $input = $form.find('[name="' + fieldName + '"]');
// 高亮字段但不各自弹窗(showAlert=false
highlightError($input, errMap[fieldName], false);
if (!$firstErr && $input.length) $firstErr = $input;
msgs.push(errMap[fieldName]);
}
if ($firstErr) $firstErr.focus().select();
showErrorDialog(msgs.join('\n'));
} else {
showErrorDialog('提交失败: ' + (res.error || '未知错误'));
}
}
},
error: function(xhr) {
var msg = '网络错误,请重试';
if (xhr.responseText) msg += '\n\n服务器返回:\n' + xhr.responseText.substring(0, 500);
showErrorDialog(msg);
}
});
});
<?php endif; ?>
<?php if ($config['customInit']): ?>
<?php echo $config['customInit']; ?>
<?php endif; ?>
// --- 自动聚焦第一个扫描字段 ---
var $fs = $('#mainForm .scan-submit').first();
if ($fs.length && $fs.val().trim() === '') setTimeout(function() { $fs.focus(); }, 300);
});
<?php if ($config['customJs']): ?>
<?php echo $config['customJs']; ?>
<?php endif; ?>
</script>
<?php
// 输出页脚框架
include APP_PATH . 'app/views/layouts/station_footer.php';
}