1243 lines
59 KiB
PHP
1243 lines
59 KiB
PHP
<?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'] ?? ''); ?>
|
||
今日录入:<span class="count-num" id="todayCount"><?php echo $todayCount; ?></span> 条
|
||
| 总共:<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';
|
||
}
|