0 && $bizType === 'workorder') { $workorder = Workorder::with('processes')->find($bizId); if (!$workorder) { throw new Exception('工单不存在'); } $product = $workorder['product_id'] ? Product::with('processes')->find($workorder['product_id']) : null; $processes = self::resolveProcesses($workorder['processes'], $product ? $product['processes'] : [], $options); $qtyDefault = $workorder['qty']; $sheetQtyDefault = $workorder['sheet_qty']; $header = [ 'workorder_id' => (int)$workorder['id'], 'workorder_no' => self::pickText($options, 'no', (string)$workorder['no']), 'batch_no' => (string)$workorder['batch_no'], 'plan_date' => (string)$workorder['plan_date'], 'customer' => (string)$workorder['customer'], 'remark' => (string)$workorder['remark'], ]; $storedName = trim((string)($workorder['product_name'] ?? '')); $nameOverride = self::pickText($options, 'name', $storedName); if ($nameOverride === '' && $product) { $nameOverride = (string)$product['name']; } if ($nameOverride !== '' && $product) { $product['name'] = $nameOverride; } elseif ($nameOverride !== '') { $product = ['id' => 0, 'code' => '', 'name' => $nameOverride, 'spec' => '', 'unit' => '']; } } elseif ($bizId > 0) { $product = Product::with('processes')->find($bizId); if (!$product) { throw new Exception('产品不存在'); } $processes = self::resolveProcesses([], $product['processes'], $options); $qtyDefault = $product['qty']; $sheetQtyDefault = $product['sheet_qty']; $nameOverride = self::pickText($options, 'name', ''); if ($nameOverride !== '') { $product['name'] = $nameOverride; } $codeOverride = self::pickText($options, 'code', self::pickText($options, 'no', '')); if ($codeOverride !== '') { $product['code'] = $codeOverride; } $header = [ 'workorder_id' => 0, 'workorder_no' => '', 'batch_no' => '', 'plan_date' => '', 'customer' => '', 'remark' => (string)$product['remark'], ]; } else { $no = self::pickText($options, 'no', self::pickText($options, 'code', '')); $name = self::pickText($options, 'name', ''); if ($no === '' && $name === '') { throw new Exception('请填写工单或产品编号、名称'); } $qtyDefault = 0; $sheetQtyDefault = 0; $processes = self::resolveProcesses([], [], $options); $product = [ 'id' => 0, 'code' => $bizType === 'product' ? $no : '', 'name' => $name !== '' ? $name : $no, 'spec' => '', 'unit' => '', ]; $header = [ 'workorder_id' => 0, 'workorder_no' => $bizType === 'workorder' ? $no : '', 'batch_no' => '', 'plan_date' => '', 'customer' => '', 'remark' => '', ]; } $qty = self::pickQty($options, $qtyDefault); $header['qty_plan'] = $qty; $sheetQty = self::pickSheetQty($options, $sheetQtyDefault); $sheets = self::splitSheets($qty, $sheetQty); $operator = User::get(FactoryContext::userId()); $processNames = []; foreach ($processes as $item) { $processNames[] = $item['name']; } $fill = [ 'factory.name' => (string)$factory['name'], 'factory.code' => (string)$factory['code'], 'factory.contact' => (string)$factory['contact'], 'factory.mobile' => (string)$factory['mobile'], 'product.id' => $product ? (int)$product['id'] : 0, 'product.code' => $product ? (string)$product['code'] : '', 'product.name' => $product ? (string)$product['name'] : '', 'product.spec' => $product ? (string)$product['spec'] : '', 'product.unit' => $product ? (string)$product['unit'] : '', 'workorder.no' => $header['workorder_no'], 'workorder.qty' => self::formatQty($qty), 'qty_plan' => self::formatQty($qty), 'workorder.qty_plan' => self::formatQty($qty), 'workorder.batch_no' => $header['batch_no'], 'workorder.pack_no' => isset($header['pack_no']) ? (string)$header['pack_no'] : '', 'workorder.plan_date' => $header['plan_date'], 'workorder.customer' => $header['customer'], 'workorder.remark' => $header['remark'], 'process_names' => implode('、', $processNames), 'sheet_qty' => self::formatQty($sheetQty), 'operator' => $operator ? (string)($operator['nickname'] ?: $operator['username']) : '', 'print_time' => date('Y-m-d H:i:s'), ]; $extra = isset($options['extra']) && is_array($options['extra']) ? $options['extra'] : []; foreach ($extra as $key => $value) { if (is_scalar($value)) { $fill[(string)$key] = (string)$value; } } $sheetRows = []; foreach ($sheets as $sheet) { $sheetFill = $fill; $sheetFill['sheet.no'] = (string)$sheet['no']; $sheetFill['sheet.total'] = (string)$sheet['total']; $sheetFill['sheet.qty'] = self::formatQty($sheet['qty']); $sheetFill['sheet.no_text'] = $sheet['no'] . '/' . $sheet['total']; $sheetFill['workorder.pack_no'] = $sheet['total'] . '-' . $sheet['no']; $sheetFill['sheet.pack_no'] = $sheet['total'] . '-' . $sheet['no']; $sheetFill['qty_plan'] = self::formatQty($qty); $sheetFill['workorder.qty_plan'] = self::formatQty($qty); $sheetRows[] = [ 'no' => $sheet['no'], 'total' => $sheet['total'], 'qty' => $sheet['qty'], 'qty_text' => self::formatQty($sheet['qty']), 'no_text' => $sheet['no'] . '/' . $sheet['total'], 'fill' => $sheetFill, ]; } $plateTotal = count($sheetRows); $from = (int)($options['sheet_from'] ?? $options['sheet_start'] ?? 0); $to = (int)($options['sheet_to'] ?? $options['sheet_end'] ?? 0); if ($from < 1) { $from = 1; } if ($to < 1 || $to > $plateTotal) { $to = $plateTotal; } if ($from > $to) { $from = 1; } $printed = []; foreach ($sheetRows as $row) { if ($row['no'] >= $from && $row['no'] <= $to) { $printed[] = $row; } } if (!$printed) { $printed = $sheetRows; } $productArr = []; if (is_object($product) && method_exists($product, 'toArray')) { $productArr = $product->toArray(); } elseif (is_array($product)) { $productArr = $product; } $workorderArr = []; if (is_object($workorder) && method_exists($workorder, 'toArray')) { $workorderArr = $workorder->toArray(); } return [ 'biz_type' => $bizType, 'biz_id' => (int)$bizId, 'factory' => [ 'id' => (int)$factory['id'], 'name' => (string)$factory['name'], 'code' => (string)$factory['code'], 'contact' => (string)$factory['contact'], 'mobile' => (string)$factory['mobile'], ], 'product' => $productArr, 'workorder' => $workorderArr, 'header' => $header, 'processes' => $processes, 'process_names' => $processNames, 'qty_plan' => $qty, 'sheet_qty' => $sheetQty, 'plate_total' => $plateTotal, 'sheet_from' => $from, 'sheet_to' => $to, 'total_sheets' => count($printed), 'sheets' => $printed, 'fill' => $fill, ]; } /** * 按模板占位符或默认表格生成每张 HTML * * @param array $plan * @param string $htmlTpl 前端传入的带 {field} 占位符的 HTML;空则用默认表格 * @param array $schema 有原稿时附带套打文字坐标 * @return array */ public static function renderPages(array $plan, $htmlTpl = '', array $schema = []) { $pages = []; foreach ($plan['sheets'] as $sheet) { if ($htmlTpl !== '') { $html = self::replacePlaceholders($htmlTpl, $sheet['fill'], $plan['processes']); } else { $html = self::defaultSheetHtml($plan, $sheet); } $pages[] = [ 'no' => $sheet['no'], 'total' => $sheet['total'], 'qty' => $sheet['qty'], 'title' => '流程单 ' . $sheet['no_text'], 'html' => $html, 'fill' => $sheet['fill'], 'print_items' => ($schema && self::isOverlay($schema)) ? self::overlayPrintItems($schema, $sheet['fill'], $plan['processes']) : [], ]; } return $pages; } /** * 读取当前模板版本排版 * @param Template $template * @return string */ public static function schemaJson(Template $template) { $versionId = (int)$template['current_version_id']; if ($versionId <= 0) { return ''; } // 官方模板 version.factory_id=0,不能走工厂隔离,否则套打读不到格子。 $json = Db::name('ps_template_version')->where('id', $versionId)->value('schema_json'); return $json ? (string)$json : ''; } /** * 从模板 schema 取出套打 HTML。 * overlay:纸上已有模板样式,只按格子位置输出填写数据。 * compose:空白纸自己搭的文字/线框/填写格。 * @param string $schemaJson * @param string $fallbackImage * @param bool $includeBackground 仅对照用;套打输出不要底图 * @return string */ public static function htmlFromSchema($schemaJson, $fallbackImage = '', $includeBackground = false) { $schema = self::decodeSchema($schemaJson, $fallbackImage); if (self::isOverlay($schema) && !empty($schema['background'])) { return self::imageSheetHtml($schema['background'], $schema, $includeBackground); } if (!empty($schema['elements'])) { return self::composeSheetHtml($schema); } if (!empty($schema['html'])) { return (string)$schema['html']; } $raw = trim((string)$schemaJson); if ($raw !== '' && stripos($raw, '<') !== false && $raw[0] !== '{') { return $raw; } return self::composeSheetHtml($schema); } /** * 有原稿图的模板:打印纸上已有样式,只输出填写字段。 * @param array $schema * @return bool */ public static function isOverlay(array $schema) { if (trim((string)($schema['background'] ?? $schema['file_url'] ?? '')) === '') { return false; } return (bool)self::fillElements($schema); } /** * 套打时真正往纸上写字的格子。 * @param array $schema * @return array */ public static function fillElements(array $schema) { $out = []; foreach ($schema['elements'] ?? [] as $el) { if (!is_array($el)) { continue; } if (($el['type'] ?? '') === 'field' || ($el['role'] ?? '') === 'fill' || trim((string)($el['key'] ?? '')) !== '') { $out[] = $el; } } if (!$out && !empty($schema['cells']) && is_array($schema['cells'])) { foreach ($schema['cells'] as $el) { if (is_array($el) && trim((string)($el['key'] ?? '')) !== '') { $out[] = $el; } } } return $out; } /** * 把库里的 schema 规整成可编辑、可套打的结构。 * @param string $schemaJson * @param string $fallbackImage * @return array */ public static function decodeSchema($schemaJson, $fallbackImage = '') { $decoded = []; $raw = trim((string)$schemaJson); if ($raw !== '') { $tmp = json_decode($raw, true); if (is_array($tmp)) { $decoded = $tmp; } } $bg = ''; foreach (['background', 'file_url', 'image', 'cover'] as $key) { if (!empty($decoded[$key])) { $bg = (string)$decoded[$key]; break; } } if ($bg === '') { $bg = (string)$fallbackImage; } $legacy = false; $page = isset($decoded['page']) && is_array($decoded['page']) ? $decoded['page'] : []; $rawElements = self::collectLayoutItems($decoded); $elements = self::normalizeElements( $rawElements, (int)($page['image_width'] ?? 0), (int)($page['image_height'] ?? 0) ); if (!$elements && !empty($decoded['fields'])) { $elements = self::elementsFromFields($decoded['fields']); $legacy = true; } $mode = (string)($decoded['mode'] ?? ''); if ($bg !== '') { $mode = 'overlay'; } elseif ($mode !== 'compose') { $mode = 'compose'; } if (!$elements && $mode === 'compose' && $bg === '') { $elements = [ self::makeElement('title', 'text', '生产流程单', '', '', 15, 4, 70, 6, 20, 'center', true, false), ]; } $elements = self::resolveQrField(self::alignFlowCardFields(self::alignHeaderFields(self::bindPrintKeys($elements)))); $fields = []; foreach ($elements as $el) { if (($el['type'] ?? '') === 'field' || ($el['key'] ?? '') !== '') { $fields[] = $el; } } $pageW = isset($page['w']) ? (float)$page['w'] : 210; $pageH = isset($page['h']) ? (float)$page['h'] : 297; if ($pageW <= 0) { $pageW = 210; } if ($pageH <= 0) { $pageH = 297; } $pageBg = self::color($page['background'] ?? ($decoded['page_background'] ?? '#ffffff'), false, '#ffffff'); $cells = []; foreach ($elements as $el) { if (($el['key'] ?? '') !== '' || ($el['role'] ?? '') === 'fill' || ($el['type'] ?? '') === 'field' || preg_match('/二维码/', (string)($el['label'] ?? '')) ) { $cells[] = [ 'id' => $el['id'], 'key' => $el['key'], 'label' => $el['label'], 'text' => $el['text'], 'x' => $el['x'], 'y' => $el['y'], 'w' => $el['w'], 'h' => $el['h'], ]; } } return [ 'name' => (string)($decoded['name'] ?? $decoded['title'] ?? ''), 'title' => (string)($decoded['title'] ?? $decoded['name'] ?? ''), 'mode' => $mode, 'page' => [ 'w' => $pageW, 'h' => $pageH, 'paper_size' => (string)($decoded['paper_size'] ?? ($page['paper_size'] ?? '')), 'dpi' => isset($page['dpi']) ? (int)$page['dpi'] : 0, 'background' => $pageBg, 'image_width' => isset($page['image_width']) ? (int)$page['image_width'] : 0, 'image_height' => isset($page['image_height']) ? (int)$page['image_height'] : 0, ], 'background' => $bg, 'file_url' => $bg, 'mark_image' => (string)($decoded['mark_image'] ?? ''), 'elements' => $elements, 'cells' => $cells, 'fields' => $fields, 'legacy_overlay' => $legacy, 'processes' => isset($decoded['processes']) && is_array($decoded['processes']) ? $decoded['processes'] : [], 'html' => (string)($decoded['html'] ?? ''), 'source' => (string)($decoded['source'] ?? ''), ]; } /** * 按百分比坐标输出填写字段。套打默认不含底图。 * @param string $imageUrl * @param array $schema * @param bool $includeImage * @return string */ public static function imageSheetHtml($imageUrl, array $schema = [], $includeImage = false) { $src = htmlspecialchars(self::printImageSrc($imageUrl), ENT_QUOTES, 'UTF-8'); $fields = self::fillElements($schema); $page = isset($schema['page']) && is_array($schema['page']) ? $schema['page'] : []; $pageW = (float)($page['w'] ?? 210); $pageH = (float)($page['h'] ?? 297); if ($pageW <= 0) { $pageW = 210; } if ($pageH <= 0) { $pageH = 297; } $typeface = self::overlayTypeface($schema); $html = '
'; if ($includeImage && $src !== '') { $html .= '流程单底图'; } foreach ($fields as $field) { $key = trim((string)($field['key'] ?? '')); $text = (string)($field['text'] ?? ''); if ($key === '' && $text === '') { continue; } $left = self::pct($field['x'] ?? 0); $top = self::pct($field['y'] ?? 0); $width = self::pct($field['w'] ?? 10); $height = self::pct($field['h'] ?? 4); $align = in_array($field['align'] ?? 'left', ['left', 'center', 'right'], true) ? $field['align'] : 'left'; $color = self::color($field['color'] ?? '#111111', false, '#111111'); $content = $key !== '' ? '{' . $key . '}' : htmlspecialchars($text, ENT_QUOTES, 'UTF-8'); $html .= '
' . $content . '
'; } $html .= '
'; return $html; } /** * 套打文字项:按格子毫米坐标输出,不含底图。 * @param array $schema * @param array $fill * @param array $processes * @return array */ public static function overlayPrintItems(array $schema, array $fill, array $processes = []) { $page = isset($schema['page']) && is_array($schema['page']) ? $schema['page'] : []; $pageW = (float)($page['w'] ?? 210); $pageH = (float)($page['h'] ?? 297); if ($pageW <= 0) { $pageW = 210; } if ($pageH <= 0) { $pageH = 297; } $fields = self::fillElements($schema); $typeface = self::overlayTypeface($schema, $fields); $items = []; foreach ($fields as $field) { $key = trim((string)($field['key'] ?? '')); $text = $key !== '' ? self::fillText($key, $fill, $processes) : (string)($field['text'] ?? ''); $text = trim($text); $xMm = self::pct($field['x'] ?? 0) / 100 * $pageW; $yMm = self::pct($field['y'] ?? 0) / 100 * $pageH; $wMm = max(8, self::pct($field['w'] ?? 10) / 100 * $pageW); $hMm = max(3, self::pct($field['h'] ?? 4) / 100 * $pageH); if ($key === 'qr') { $payload = self::qrPayload($fill); if ($payload === '') { continue; } $qrW = max(8, self::pct($field['w'] ?? 10) / 100 * $pageW); $qrH = max(8, self::pct($field['h'] ?? 4) / 100 * $pageH); $items[] = array_merge( ['type' => 'qrcode', 'text' => $payload], self::centerQrInCell($xMm, $yMm, $qrW, $qrH) ); continue; } if ($text === '') { continue; } // 识版色块常贴着上框线,略向下收进格内,避免字压在表格线上。 $padY = min(1.1, $hMm * 0.18); $padX = min(0.8, $wMm * 0.04); $yMm += $padY; $xMm += $padX; $hMm = max(3.2, $hMm - $padY * 1.4); $wMm = max(6, $wMm - $padX); $align = $field['align'] ?? 'left'; $items[] = [ 'top' => round($yMm, 2) . 'mm', 'left' => round($xMm, 2) . 'mm', 'width' => round($wMm, 2) . 'mm', 'height' => round($hMm, 2) . 'mm', 'text' => $text, 'font' => $typeface['family'], 'size' => $typeface['pt'], 'align' => $align === 'center' ? 2 : ($align === 'right' ? 3 : 1), 'bold' => $typeface['bold'], ]; } return $items; } /** * 二维码居中于识版格子内(不做文字字段的内边距)。 * @param float $xMm * @param float $yMm * @param float $wMm * @param float $hMm * @return array{top:string,left:string,width:string,height:string} */ protected static function centerQrInCell($xMm, $yMm, $wMm, $hMm) { $wMm = max(8, (float)$wMm); $hMm = max(8, (float)$hMm); $inset = min(1.0, $wMm * 0.04, $hMm * 0.04); $innerW = max(8, $wMm - $inset * 2); $innerH = max(8, $hMm - $inset * 2); $size = min($innerW, $innerH); $left = $xMm + ($wMm - $size) / 2; $top = $yMm + ($hMm - $size) / 2; return [ 'top' => round($top, 2) . 'mm', 'left' => round($left, 2) . 'mm', 'width' => round($size, 2) . 'mm', 'height' => round($size, 2) . 'mm', ]; } /** * 同一张套打单上的填写文字用同一字体、同一字号、同一字重。 * @param array $schema * @param array|null $fields * @return array{family:string,css:string,px:int,pt:int,bold:int} */ protected static function overlayTypeface(array $schema, array $fields = null) { if ($fields === null) { $fields = self::fillElements($schema); } $sizes = []; foreach ($fields as $field) { if (trim((string)($field['key'] ?? '')) === 'qr') { continue; } $px = max(8, (int)($field['fontSize'] ?? 13)); $sizes[] = $px; } $px = 13; if ($sizes) { $freq = array_count_values($sizes); arsort($freq); $px = (int)key($freq); } $pt = (int)max(8, min(12, round($px * 72 / 96))); return [ 'family' => self::overlayFontFamily(), 'css' => self::overlayFontCss(), 'px' => $px, 'pt' => $pt, 'bold' => 0, ]; } /** * 套打 overlay 专用字体(不影响 compose 排版)。 * @return string */ protected static function overlayFontFamily() { return '微软雅黑'; } /** * @return string */ protected static function overlayFontCss() { return "'Microsoft YaHei','微软雅黑',sans-serif"; } /** * 流程单二维码:写入可被微信打开的信息页地址(纯文本微信会提示无法识别)。 * @param array $fill * @return string */ public static function qrPayload(array $fill) { $no = trim((string)($fill['workorder.no'] ?? '')); if ($no === '') { $no = trim((string)($fill['workorder.batch_no'] ?? '')); } $data = [ 'no' => $no, 'name' => trim((string)($fill['product.name'] ?? '')), 'qty' => trim((string)($fill['qty_plan'] ?? ($fill['workorder.qty'] ?? ''))), 'plate' => trim((string)($fill['sheet_qty'] ?? '')), 'total' => trim((string)($fill['sheet.total'] ?? '')), 'cur' => trim((string)($fill['sheet.no'] ?? '')), ]; $token = self::storeQrCard($data); if ($token !== '') { return self::qrBaseUrl() . '/psqr.php?k=' . $token; } $packed = rtrim(strtr(base64_encode(json_encode($data, JSON_UNESCAPED_UNICODE)), '+/', '-_'), '='); return self::qrBaseUrl() . '/psqr.php?d=' . $packed; } /** * 微信手机必须能访问该地址。可在 extra/ps.php 的 qr_base_url 写成 http://processSheet.com * @return string */ protected static function qrBaseUrl() { $cfg = trim((string)(Config::get('site.ps_qr_base') ?: (Config::get('ps.qr_base_url') ?: ''))); if ($cfg !== '') { return rtrim($cfg, '/'); } try { $req = request(); return rtrim($req->domain() . $req->root(), '/'); } catch (\Exception $e) { return ''; } } /** * 短码存流程单信息,二维码只带短地址,格子小也能扫清。 * @param array $data * @return string */ protected static function storeQrCard(array $data) { $token = substr(bin2hex(random_bytes(5)), 0, 10); $json = json_encode($data, JSON_UNESCAPED_UNICODE); $dirs = [ RUNTIME_PATH . 'psqr' . DIRECTORY_SEPARATOR, ROOT_PATH . 'public' . DIRECTORY_SEPARATOR . 'uploads' . DIRECTORY_SEPARATOR . 'psqr' . DIRECTORY_SEPARATOR, ]; foreach ($dirs as $dir) { if (!is_dir($dir)) { @mkdir($dir, 0755, true); } if (@file_put_contents($dir . $token . '.json', $json) !== false) { return $token; } } return ''; } /** * @param string $key * @param array $fill * @param array $processes * @return string */ public static function fillText($key, array $fill, array $processes = []) { if (preg_match('/^process\.(\d+)(?:\.(name|code))?$/', $key, $m)) { $idx = (int)$m[1] - 1; if (!isset($processes[$idx])) { return ''; } $attr = !empty($m[2]) ? $m[2] : 'name'; return (string)($processes[$idx][$attr] ?? ''); } return isset($fill[$key]) ? (string)$fill[$key] : ''; } /** * @param string $path * @return string */ public static function printImageSrc($path) { $path = trim((string)$path); if ($path === '') { return ''; } if (preg_match('#^(https?:)?//#i', $path) || strpos($path, 'data:') === 0) { return $path; } return cdnurl($path, true); } /** * 白底排版:把拆出的文字、填写格、线框画成可打印 HTML。 * @param array $schema * @return string */ public static function composeSheetHtml(array $schema) { $page = isset($schema['page']) && is_array($schema['page']) ? $schema['page'] : ['w' => 210, 'h' => 297]; $pageW = (float)($page['w'] ?? 210); $pageH = (float)($page['h'] ?? 297); if ($pageW <= 0) { $pageW = 210; } if ($pageH <= 0) { $pageH = 297; } $elements = self::normalizeElements($schema['elements'] ?? []); $pageBg = self::color($page['background'] ?? '#ffffff', false, '#ffffff'); $html = '
'; foreach ($elements as $el) { $html .= self::elementHtml($el); } $html .= '
'; return $html; } /** * 合并 cells/elements 等识版结果。 * @param array $decoded * @return array */ public static function collectLayoutItems(array $decoded) { if (!empty($decoded['cells']) && is_array($decoded['cells'])) { return array_values($decoded['cells']); } $list = []; foreach (['elements', 'blocks', 'items', 'nodes'] as $key) { if (empty($decoded[$key]) || !is_array($decoded[$key])) { continue; } foreach ($decoded[$key] as $item) { $list[] = $item; } } return $list; } /** * @param mixed $items * @param int $imageW * @param int $imageH * @return array */ public static function normalizeElements($items, $imageW = 0, $imageH = 0) { if (!is_array($items)) { return []; } list($sx, $sy) = self::axisScale($items, $imageW, $imageH); $list = []; $i = 0; foreach ($items as $item) { $i++; if (is_string($item) && trim($item) !== '') { $key = self::mapFieldKey($item); $list[] = self::buildElement([ 'id' => 'e' . $i, 'type' => $key !== '' ? 'field' : 'text', 'text' => $key !== '' ? '' : $item, 'key' => $key, 'x' => 8, 'y' => 6 + ($i - 1) * 5, 'w' => 30, 'h' => 4, ], 1); continue; } if (!is_array($item)) { continue; } $item['id'] = $item['id'] ?? ('e' . $i); foreach (['x' => $sx, 'w' => $sx, 'width' => $sx, 'y' => $sy, 'h' => $sy, 'height' => $sy] as $k => $scale) { if (isset($item[$k]) && is_numeric($item[$k])) { $item[$k] = (float)$item[$k] * $scale; } } $list[] = self::buildElement($item, 1); } return self::pruneNoise($list); } /** * @param array $items * @param int $imageW * @param int $imageH * @return array [sx, sy] */ protected static function axisScale($items, $imageW = 0, $imageH = 0) { $maxX = 0; $maxY = 0; foreach ($items as $item) { if (!is_array($item)) { continue; } $x = (float)($item['x'] ?? 0); $y = (float)($item['y'] ?? 0); $w = (float)($item['w'] ?? ($item['width'] ?? 0)); $h = (float)($item['h'] ?? ($item['height'] ?? 0)); $maxX = max($maxX, $x, $x + $w, $w); $maxY = max($maxY, $y, $y + $h, $h); } if ($maxX <= 1.5 && $maxY <= 1.5 && ($maxX > 0 || $maxY > 0)) { return [100, 100]; } if ($maxX > 110 || $maxY > 110) { $denX = max((float)$imageW, $maxX, 1); $denY = max((float)$imageH, $maxY, 1); return [100 / $denX, 100 / $denY]; } return [1, 1]; } /** * 模型常刷出成百条废线,只保留能当表格分隔的。 * @param array $list * @return array */ protected static function pruneNoise(array $list) { $lines = []; $others = []; foreach ($list as $el) { if (($el['type'] ?? '') === 'line') { $lines[] = $el; } else { $others[] = $el; } } if (count($lines) <= 36) { return array_merge($others, $lines); } $kept = []; foreach ($lines as $el) { if (max((float)$el['w'], (float)$el['h']) >= 8) { $kept[] = $el; } } if (count($kept) > 36) { $kept = array_slice($kept, 0, 36); } return array_merge($others, $kept); } /** * @param mixed $fields * @return array */ public static function elementsFromFields($fields) { $list = []; foreach (self::normalizeOverlayFields($fields, false) as $field) { $type = $field['key'] !== '' ? 'field' : 'text'; $list[] = self::buildElement([ 'id' => $field['id'], 'type' => $type, 'text' => $field['text'], 'key' => $field['key'], 'label' => $field['label'], 'x' => $field['x'], 'y' => $field['y'], 'w' => $field['w'], 'h' => $field['h'], 'fontSize' => $field['fontSize'], 'align' => $field['align'], 'border' => $type === 'field', ], 1); } return $list; } /** * 套打可绑定字段 * @return array */ public static function fieldCatalog() { return [ ['key' => 'factory.name', 'label' => '工厂名称'], ['key' => 'factory.code', 'label' => '工厂编码'], ['key' => 'factory.contact', 'label' => '联系人'], ['key' => 'factory.mobile', 'label' => '工厂电话'], ['key' => 'product.code', 'label' => '产品编码'], ['key' => 'product.name', 'label' => '产品名称'], ['key' => 'product.spec', 'label' => '规格'], ['key' => 'product.unit', 'label' => '单位'], ['key' => 'workorder.no', 'label' => '工单号'], ['key' => 'workorder.qty', 'label' => '工单数量'], ['key' => 'qty_plan', 'label' => '总数量'], ['key' => 'workorder.qty_plan', 'label' => '计划总量'], ['key' => 'workorder.batch_no', 'label' => '批次'], ['key' => 'workorder.pack_no', 'label' => '码包号'], ['key' => 'qr', 'label' => '二维码区域'], ['key' => 'workorder.plan_date', 'label' => '计划日期'], ['key' => 'workorder.customer', 'label' => '客户'], ['key' => 'workorder.remark', 'label' => '备注'], ['key' => 'sheet.qty', 'label' => '本张数量'], ['key' => 'sheet.no_text', 'label' => '页码'], ['key' => 'process_names', 'label' => '工序'], ['key' => 'operator', 'label' => '操作员'], ['key' => 'print_time', 'label' => '打印时间'], ]; } /** * @param mixed $fields * @return array */ public static function normalizeOverlayFields($fields, $fillDefault = true) { $catalog = []; foreach (self::fieldCatalog() as $item) { $catalog[$item['key']] = $item['label']; } $list = []; if (!is_array($fields)) { $fields = []; } $scale = self::coordScale($fields); $i = 0; $y = 8; foreach ($fields as $item) { $i++; if (is_string($item) && trim($item) !== '') { $key = self::mapFieldKey($item); $list[] = self::makeField('f' . $i, $key, $catalog[$key] ?? $item, 52, $y, 30, 3.6); $y += 5.2; continue; } if (!is_array($item)) { continue; } $key = self::mapFieldKey($item['key'] ?? $item['k'] ?? $item['name'] ?? ''); $label = trim((string)($item['label'] ?? '')); if ($label === '' && $key !== '' && isset($catalog[$key])) { $label = $catalog[$key]; } $id = (string)($item['id'] ?? ('f' . $i)); $list[] = self::makeField( $id, $key, $label, isset($item['x']) ? $item['x'] * $scale : 52, isset($item['y']) ? $item['y'] * $scale : $y, isset($item['w']) ? $item['w'] * $scale : (isset($item['width']) ? $item['width'] * $scale : 30), isset($item['h']) ? $item['h'] * $scale : (isset($item['height']) ? $item['height'] * $scale : 3.6), isset($item['fontSize']) ? $item['fontSize'] : 13, isset($item['align']) ? $item['align'] : 'left', isset($item['text']) ? $item['text'] : '' ); $y += 5.2; } if (!$list && $fillDefault) { foreach (['product.name', 'product.code', 'workorder.no', 'sheet.qty', 'workorder.plan_date', 'operator'] as $idx => $key) { $list[] = self::makeField('f' . ($idx + 1), $key, $catalog[$key], 52, 8 + $idx * 5.2, 30, 3.6); } } return $list; } /** * @param string $name * @return string */ public static function mapFieldKey($name) { $name = trim((string)$name); $alias = [ '产品名称' => 'product.name', '产品' => 'product.name', '品名' => 'product.name', '名称' => 'product.name', '产品编码' => 'product.code', '编码' => 'product.code', '货号' => 'product.code', '规格' => 'product.spec', '单位' => 'product.unit', '工厂' => 'factory.name', '厂名' => 'factory.name', '工单号' => 'workorder.no', '单号' => 'workorder.no', '流程单号' => 'workorder.no', '订单号' => 'workorder.no', '销售订单号' => 'workorder.no', '制单号' => 'workorder.no', '数量' => 'qty_plan', '总数量' => 'qty_plan', '总投料数' => 'qty_plan', '计划总量' => 'qty_plan', '本张数量' => 'sheet.qty', '投料' => 'sheet.qty', '批次' => 'workorder.batch_no', '生产批次号' => 'workorder.batch_no', '码包号' => 'workorder.pack_no', '包号' => 'workorder.pack_no', '客户' => 'workorder.customer', '日期' => 'workorder.plan_date', '计划日' => 'workorder.plan_date', '备注' => 'workorder.remark', '操作员' => 'operator', '时间' => 'print_time', '页码' => 'sheet.no_text', '工序' => 'process_names', ]; if (preg_match('/^二维码/', $name) || $name === 'QR' || $name === 'qr') { return 'qr'; } if (preg_match('/^工序\s*([一二三四五六七八九十百零〇两\d]+)$/u', $name, $m)) { $n = self::chineseToInt($m[1]); if ($n > 0) { return 'process.' . $n . '.name'; } } if (preg_match('/^process\.\d+\.(name|code)$/', $name)) { return $name; } if (isset($alias[$name])) { return $alias[$name]; } $allowed = array_column(self::fieldCatalog(), 'key'); return in_array($name, $allowed, true) ? $name : ''; } /** * 把「工序一」等格子绑到 process.N.name,把总数量绑到计划总量。 * @param array $elements * @return array */ protected static function bindPrintKeys(array $elements) { $seq = 1; foreach ($elements as &$el) { if (!is_array($el)) { continue; } $label = trim((string)($el['label'] ?? '')); $key = trim((string)($el['key'] ?? '')); if ($label !== '') { $mapped = self::mapFieldKey($label); if ($mapped !== '') { $key = $mapped; } } if ($key === 'process_names' || preg_match('/^工序/', $label)) { if (!preg_match('/^process\.\d+/', $key)) { $n = 0; if (preg_match('/^工序\s*([一二三四五六七八九十百零〇两\d]+)$/u', $label, $m)) { $n = self::chineseToInt($m[1]); } $key = 'process.' . ($n > 0 ? $n : $seq) . '.name'; } } if (preg_match('/^process\.(\d+)/', $key, $m)) { $seq = max($seq, (int)$m[1] + 1); } if (in_array($label, ['总数量', '总投料数', '计划总量'], true)) { $key = 'qty_plan'; } $el['key'] = $key; } unset($el); return $elements; } /** * 页眉:最宽的横条是备注;顶部一排三个小格是批次 / 订单 / 码包号。 * @param array $elements * @return array */ protected static function alignHeaderFields(array $elements) { $wideIdx = null; $wideW = 48; $topSmall = []; foreach ($elements as $idx => $el) { if (!is_array($el)) { continue; } $w = (float)($el['w'] ?? 0); $h = (float)($el['h'] ?? 0); $y = (float)($el['y'] ?? 0); $x = (float)($el['x'] ?? 0); if ($w > $wideW && $h < 10) { $wideW = $w; $wideIdx = $idx; } if ($y < 22 && $w >= 10 && $w <= 28 && $h >= 2.5 && $h <= 8) { $topSmall[] = $idx; } } if ($wideIdx !== null) { $elements[$wideIdx]['key'] = 'workorder.remark'; $elements[$wideIdx]['label'] = '备注'; } usort($topSmall, function ($a, $b) use ($elements) { return ((float)$elements[$a]['x'] <=> (float)$elements[$b]['x']); }); if (count($topSmall) >= 3) { $keys = [ ['workorder.batch_no', '生产批次号'], ['workorder.no', '订单号'], ['workorder.pack_no', '码包号'], ]; for ($i = 0; $i < 3; $i++) { $idx = $topSmall[$i]; $elements[$idx]['key'] = $keys[$i][0]; $elements[$idx]['label'] = $keys[$i][1]; } } return $elements; } /** * 流程卡右侧大块空白通常是二维码区;优先绑定面积最大的右侧格子。 * @param array $elements * @return array */ protected static function resolveQrField(array $elements) { $reserved = [ 'product.name', 'product.code', 'product.spec', 'qty_plan', 'workorder.batch_no', 'workorder.no', 'workorder.pack_no', 'workorder.remark', 'sheet.qty', 'sheet.no_text', 'operator', 'print_time', 'process_names', ]; $qrIndices = []; $bestIdx = null; $bestArea = 0; foreach ($elements as $idx => $el) { if (!is_array($el)) { continue; } $key = trim((string)($el['key'] ?? '')); $label = trim((string)($el['label'] ?? '')); if ($key === 'qr' || preg_match('/二维码/u', $label)) { $qrIndices[] = $idx; } $x = (float)($el['x'] ?? 0); $y = (float)($el['y'] ?? 0); $w = (float)($el['w'] ?? 0); $h = (float)($el['h'] ?? 0); if ($x < 30 || $w < 12 || $h < 12) { continue; } if ($y < 6 && $h < 16) { continue; } if ($key !== '' && ($key === 'qr' || preg_match('/^process\./', $key) || in_array($key, $reserved, true))) { if ($key !== 'qr') { continue; } } $area = $w * $h; if ($area > $bestArea) { $bestArea = $area; $bestIdx = $idx; } } if ($bestIdx === null) { return $elements; } foreach ($qrIndices as $idx) { if ($idx === $bestIdx) { continue; } $area = (float)($elements[$idx]['w'] ?? 0) * (float)($elements[$idx]['h'] ?? 0); if ($area < $bestArea * 0.75) { $elements[$idx]['key'] = ''; } } $elements[$bestIdx]['key'] = 'qr'; $elements[$bestIdx]['label'] = '二维码区域'; $elements[$bestIdx]['type'] = 'field'; $elements[$bestIdx]['role'] = 'fill'; return $elements; } /** * 产品工序流程卡:色块标签常把「产品名称 / 总投料数」和工序列认反。 * 按位置纠正——工序名称列是左侧一叠格子,其上一行从左到右是名称、总投料数、规格。 * @param array $elements * @return array */ protected static function alignFlowCardFields(array $elements) { $candidates = []; foreach ($elements as $idx => $el) { if (!is_array($el)) { continue; } $w = (float)($el['w'] ?? 0); $h = (float)($el['h'] ?? 0); if ($w > 45 || $h > 10) { continue; } if (($el['key'] ?? '') === '' && ($el['role'] ?? '') !== 'fill' && ($el['type'] ?? '') !== 'field') { continue; } $candidates[] = $idx; } if (count($candidates) < 8) { return $elements; } $byX = []; foreach ($candidates as $idx) { $xBand = (int)round((float)$elements[$idx]['x']); $byX[$xBand][] = $idx; } $stack = []; foreach ($byX as $ids) { if (count($ids) < 8) { continue; } usort($ids, function ($a, $b) use ($elements) { return ((float)$elements[$a]['y'] <=> (float)$elements[$b]['y']); }); if (count($ids) > count($stack)) { $stack = $ids; } } if (count($stack) < 8) { return $elements; } $stackTop = (float)$elements[$stack[0]]['y']; $headerRow = []; foreach ($candidates as $idx) { if (in_array($idx, $stack, true)) { continue; } $y = (float)$elements[$idx]['y']; $h = (float)$elements[$idx]['h']; if ($y + $h <= $stackTop + 0.8 && $y >= $stackTop - 12) { $headerRow[] = $idx; } } usort($headerRow, function ($a, $b) use ($elements) { return ((float)$elements[$a]['x'] <=> (float)$elements[$b]['x']); }); if (count($headerRow) >= 2) { $elements[$headerRow[0]]['key'] = 'product.name'; $elements[$headerRow[0]]['label'] = '产品名称'; $elements[$headerRow[1]]['key'] = 'qty_plan'; $elements[$headerRow[1]]['label'] = '总投料数'; if (isset($headerRow[2])) { $elements[$headerRow[2]]['key'] = 'product.spec'; $elements[$headerRow[2]]['label'] = '规格'; } } $seq = 1; foreach ($stack as $idx) { $elements[$idx]['key'] = 'process.' . $seq . '.name'; $elements[$idx]['label'] = '工序' . $seq; $seq++; } return $elements; } /** * @param string $text * @return int */ public static function chineseToInt($text) { $text = trim((string)$text); if ($text === '') { return 0; } if (preg_match('/^\d+$/', $text)) { return (int)$text; } $d = ['零' => 0, '〇' => 0, '一' => 1, '二' => 2, '两' => 2, '三' => 3, '四' => 4, '五' => 5, '六' => 6, '七' => 7, '八' => 8, '九' => 9]; if (isset($d[$text])) { return $d[$text]; } if ($text === '十') { return 10; } if (preg_match('/^十([一二三四五六七八九])$/u', $text, $m)) { return 10 + $d[$m[1]]; } if (preg_match('/^([一二三四五六七八九])十([一二三四五六七八九])?$/u', $text, $m)) { return $d[$m[1]] * 10 + (!empty($m[2]) ? $d[$m[2]] : 0); } return 0; } protected static function makeField($id, $key, $label, $x, $y, $w, $h, $fontSize = 13, $align = 'left', $text = '') { return [ 'id' => (string)$id, 'key' => (string)$key, 'label' => (string)$label, 'x' => self::pct($x), 'y' => self::pct($y), 'w' => max(4, self::pct($w)), 'h' => max(2, self::pct($h)), 'fontSize' => max(8, (int)$fontSize), 'align' => $align === 'center' || $align === 'right' ? $align : 'left', 'text' => (string)$text, ]; } protected static function makeElement($id, $type, $text, $key, $label, $x, $y, $w, $h, $fontSize = 13, $align = 'left', $bold = false, $border = false) { return self::buildElement([ 'id' => $id, 'type' => $type, 'text' => $text, 'key' => $key, 'label' => $label, 'x' => $x, 'y' => $y, 'w' => $w, 'h' => $h, 'fontSize' => $fontSize, 'align' => $align, 'bold' => $bold, 'border' => $border, ], 1); } /** * 把模型输出的一块规整成可还原、可套打的元素。 * @param array $item * @param float $scale * @return array */ public static function buildElement(array $item, $scale = 1) { $style = isset($item['style']) && is_array($item['style']) ? $item['style'] : []; $pick = function ($keys, $default = null) use ($item, $style) { foreach ((array)$keys as $key) { if (array_key_exists($key, $item) && $item[$key] !== '' && $item[$key] !== null) { return $item[$key]; } if (array_key_exists($key, $style) && $style[$key] !== '' && $style[$key] !== null) { return $style[$key]; } } return $default; }; $type = strtolower((string)$pick(['type', 'kind'], '')); $typeMap = [ 'label' => 'text', 'title' => 'text', 'static' => 'text', 'word' => 'text', 'input' => 'field', 'blank' => 'field', 'fill' => 'field', 'placeholder' => 'field', 'hr' => 'line', 'vline' => 'line', 'hline' => 'line', 'rule' => 'line', 'rect' => 'box', 'border' => 'box', 'frame' => 'box', 'td' => 'cell', 'gridcell' => 'cell', ]; if (isset($typeMap[$type])) { $type = $typeMap[$type]; } if ($type === '') { $type = 'cell'; } $key = self::mapFieldKey((string)$pick(['key', 'k', 'bind'], '')); $text = (string)$pick(['text', 'content', 'value'], ''); $role = strtolower((string)$pick(['role'], '')); if ($role === 'fill' || $role === 'input' || $role === 'blank') { $role = 'fill'; } elseif ($role === 'label' || $role === 'header' || $role === 'title') { $role = 'label'; } else { $role = ''; } if ($type === '' && $key !== '') { $type = 'field'; } $forceBorder = false; if ($type === 'cell' && ($role === 'fill' || $key !== '')) { $type = 'field'; $forceBorder = true; } if ($type === 'cell') { $type = 'text'; $forceBorder = true; } if ($type === '' && $text !== '') { $type = 'text'; } if ($type === '') { $type = 'text'; } if ($type === 'text' && $key !== '' && trim($text) === '') { $type = 'field'; } if ($role === '') { $role = ($type === 'field' || $key !== '') ? 'fill' : 'label'; } $label = trim((string)$pick(['label'], '')); if ($label === '' && $key !== '') { foreach (self::fieldCatalog() as $cat) { if ($cat['key'] === $key) { $label = $cat['label']; break; } } } $minW = $type === 'line' ? 0.2 : 1.2; $minH = $type === 'line' ? 0.2 : 1.2; $borderFlag = $pick(['border'], null); $borders = $pick(['borders'], ''); $borderWidth = (int)$pick(['borderWidth', 'border_width'], 0); if ($borderFlag === null) { $borderFlag = $forceBorder || in_array($type, ['field', 'box'], true) || $borders !== '' || $borderWidth > 0; } $borderFlag = (bool)$borderFlag; if ($borderFlag && $borderWidth < 1) { $borderWidth = 1; } if (!$borderFlag) { $borderWidth = 0; $borders = ''; } elseif ($borders === '' || $borders === null) { $borders = 'tblr'; } $bg = $pick(['background', 'bg', 'fillColor'], $type === 'field' ? '#ffffff' : 'transparent'); if ($bg === 'fill' || $bg === 'field') { $bg = '#ffffff'; } return [ 'id' => (string)$pick(['id'], 'e1'), 'type' => in_array($type, ['text', 'field', 'line', 'box'], true) ? $type : 'text', 'role' => $role, 'text' => $text, 'key' => $key, 'label' => $label, 'x' => self::pct($pick(['x'], 8) * $scale), 'y' => self::pct($pick(['y'], 6) * $scale), 'w' => max($minW, self::pct(($pick(['w', 'width'], $type === 'line' ? 80 : 18)) * $scale)), 'h' => max($minH, self::pct(($pick(['h', 'height'], $type === 'line' ? 0.4 : 4)) * $scale)), 'fontSize' => max(8, (int)$pick(['fontSize', 'size', 'font_size'], 13)), 'align' => in_array($pick(['align', 'textAlign'], 'left'), ['center', 'right'], true) ? $pick(['align', 'textAlign'], 'left') : 'left', 'bold' => (!empty($item['bold']) || ($style['bold'] ?? false) || $pick(['fontWeight'], '') === 'bold') ? 1 : 0, 'border' => $borderFlag ? 1 : 0, 'color' => self::color($pick(['color', 'textColor'], '#111111'), false, '#111111'), 'background' => self::color($bg, true, 'transparent'), 'borderColor' => self::color($pick(['borderColor', 'stroke', 'lineColor'], '#000000'), false, '#000000'), 'borderWidth' => $borderWidth, 'borders' => is_array($borders) ? self::bordersToString($borders) : strtolower((string)$borders), 'fontFamily' => self::fontFamily($pick(['fontFamily', 'font', 'font_family'], 'SimSun')), 'row' => (int)$pick(['row', 'r'], 0), 'col' => (int)$pick(['col', 'c'], 0), ]; } protected static function elementHtml(array $el) { $left = self::pct($el['x']); $top = self::pct($el['y']); $width = self::pct($el['w']); $height = self::pct($el['h']); $type = $el['type'] ?? 'text'; $color = self::color($el['color'] ?? '#111111', false, '#111111'); $bg = self::color($el['background'] ?? 'transparent', true, 'transparent'); $base = 'position:absolute;left:' . $left . '%;top:' . $top . '%;width:' . $width . '%;height:' . $height . '%;box-sizing:border-box;'; if ($type === 'line') { $vertical = $height > $width; $bc = self::color($el['borderColor'] ?? $color, false, '#000000'); $bw = max(1, (int)($el['borderWidth'] ?? 1)); $style = $base . ($vertical ? ('border-left:' . $bw . 'px solid ' . $bc . ';') : ('border-top:' . $bw . 'px solid ' . $bc . ';')); return '
'; } $border = self::borderCss($el); if ($type === 'box' && $border === '') { $border = 'border:1px solid ' . self::color($el['borderColor'] ?? '#000000', false, '#000000') . ';'; } $size = max(8, (int)$el['fontSize']); $align = in_array($el['align'], ['left', 'center', 'right'], true) ? $el['align'] : 'left'; $justify = $align === 'center' ? 'center' : ($align === 'right' ? 'flex-end' : 'flex-start'); $weight = !empty($el['bold']) ? 'bold' : 'normal'; $font = htmlspecialchars(self::fontFamily($el['fontFamily'] ?? 'SimSun'), ENT_QUOTES, 'UTF-8'); $pad = $border !== '' ? 'padding:1px 4px;' : ''; $content = ''; if (($type === 'field' || ($el['role'] ?? '') === 'fill') && ($el['key'] ?? '') !== '') { $content = '{' . $el['key'] . '}'; } else { $content = htmlspecialchars((string)($el['text'] ?? ''), ENT_QUOTES, 'UTF-8'); } $bgCss = ($bg !== '' && $bg !== 'transparent') ? 'background:' . $bg . ';' : ''; return '
' . $content . '
'; } protected static function borderCss(array $el) { $width = (int)($el['borderWidth'] ?? 0); if ($width < 1 && !empty($el['border'])) { $width = 1; } if ($width < 1) { return ''; } $color = self::color($el['borderColor'] ?? '#000000', false, '#000000'); $sides = strtolower((string)($el['borders'] ?? 'tblr')); if ($sides === '1' || $sides === 'true' || $sides === 'all') { $sides = 'tblr'; } $map = ['t' => 'top', 'r' => 'right', 'b' => 'bottom', 'l' => 'left']; $css = ''; foreach ($map as $k => $name) { if (strpos($sides, $k) !== false) { $css .= 'border-' . $name . ':' . $width . 'px solid ' . $color . ';'; } } return $css; } protected static function bordersToString($borders) { $out = ''; $map = ['t' => 't', 'top' => 't', 'r' => 'r', 'right' => 'r', 'b' => 'b', 'bottom' => 'b', 'l' => 'l', 'left' => 'l']; foreach ($map as $k => $ch) { if (!empty($borders[$k])) { $out .= $ch; } } $out = count_chars($out, 3); return $out !== '' ? $out : 'tblr'; } protected static function fontFamily($name) { $name = trim((string)$name); $allow = ['SimSun', 'SimHei', 'KaiTi', 'Microsoft YaHei', '微软雅黑', 'FangSong', 'Arial', 'serif', 'sans-serif']; foreach ($allow as $item) { if (strcasecmp($item, $name) === 0) { return $item === 'Microsoft YaHei' ? '微软雅黑' : $item; } } if (preg_match('/微软雅黑|微软黑|yahei/i', $name)) { return '微软雅黑'; } if (preg_match('/宋体|song/i', $name)) { return 'SimSun'; } if (preg_match('/黑体|^hei$/i', $name)) { return 'SimHei'; } if (preg_match('/楷/i', $name)) { return 'KaiTi'; } return 'SimSun'; } /** * @param mixed $value * @param bool $allowTransparent * @param string $default * @return string */ protected static function color($value, $allowTransparent = false, $default = '#111111') { $value = trim((string)$value); if ($value === '') { return $default; } $low = strtolower($value); if ($allowTransparent && in_array($low, ['transparent', 'none', ''], true)) { return 'transparent'; } $named = [ 'black' => '#000000', 'white' => '#ffffff', 'red' => '#cc0000', 'blue' => '#0033cc', 'gray' => '#666666', 'grey' => '#666666', 'green' => '#008000', 'yellow' => '#c9a227', ]; if (isset($named[$low])) { return $named[$low]; } if (preg_match('/^#([0-9a-f]{3})$/i', $value, $m)) { $h = $m[1]; return '#' . $h[0] . $h[0] . $h[1] . $h[1] . $h[2] . $h[2]; } if (preg_match('/^#([0-9a-f]{6})$/i', $value)) { return strtolower($value); } if (preg_match('/rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)/i', $value, $m)) { return sprintf('#%02x%02x%02x', min(255, (int)$m[1]), min(255, (int)$m[2]), min(255, (int)$m[3])); } return $default; } protected static function coordScale($items) { $maxCoord = 0; if (!is_array($items)) { return 1; } foreach ($items as $item) { if (!is_array($item)) { continue; } foreach (['x', 'y', 'w', 'h', 'width', 'height'] as $k) { if (isset($item[$k])) { $maxCoord = max($maxCoord, (float)$item[$k]); } } } return ($maxCoord > 0 && $maxCoord <= 1.5) ? 100 : 1; } protected static function pct($value) { $n = round((float)$value, 2); if ($n < 0) { return 0; } if ($n > 100) { return 100; } return $n; } /** * 工单工序优先,没有则回退产品工序(对应旧系统按印件取历史工艺) * @param mixed $workorderProcesses * @param mixed $productProcesses * @param array $options * @return array */ protected static function resolveProcesses($workorderProcesses, $productProcesses, array $options) { if (array_key_exists('processes', $options) && is_array($options['processes'])) { $list = []; $seq = 1; foreach ($options['processes'] as $item) { if (is_string($item) && trim($item) !== '') { $list[] = [ 'seq' => $seq++, 'code' => '', 'name' => trim($item), 'workstation' => '', 'standard_time' => '', 'qc_points' => '', 'remark' => '', ]; } elseif (is_array($item) && (trim((string)($item['name'] ?? '')) !== '' || trim((string)($item['code'] ?? '')) !== '')) { $name = trim((string)($item['name'] ?? '')); $code = trim((string)($item['code'] ?? '')); $list[] = [ 'seq' => isset($item['seq']) && $item['seq'] !== '' ? (int)$item['seq'] : $seq++, 'code' => $code, 'name' => $name !== '' ? $name : $code, 'workstation' => isset($item['workstation']) ? (string)$item['workstation'] : '', 'standard_time' => isset($item['standard_time']) ? (string)$item['standard_time'] : '', 'qc_points' => isset($item['qc_points']) ? (string)$item['qc_points'] : '', 'remark' => isset($item['remark']) ? (string)$item['remark'] : '', ]; } } return $list; } $string = isset($options['process_string']) ? $options['process_string'] : (isset($options['string']) ? $options['string'] : ''); if (is_string($string) && trim($string) !== '') { $names = array_values(array_filter(array_map('trim', explode(',', $string)))); return self::resolveProcesses([], [], ['processes' => $names]); } $source = []; if ($workorderProcesses && count($workorderProcesses)) { $source = $workorderProcesses; } elseif ($productProcesses && count($productProcesses)) { $source = $productProcesses; } $list = []; foreach ($source as $row) { $list[] = [ 'seq' => (int)$row['seq'], 'code' => (string)($row['code'] ?? ''), 'name' => (string)$row['name'], 'workstation' => (string)$row['workstation'], 'standard_time' => (string)$row['standard_time'], 'qc_points' => (string)$row['qc_points'], 'remark' => (string)$row['remark'], ]; } return $list; } protected static function pickText(array $options, $key, $default = '') { if (!isset($options[$key]) || $options[$key] === null) { return (string)$default; } $text = trim((string)$options[$key]); return $text !== '' ? $text : (string)$default; } protected static function pickQty(array $options, $default) { if (isset($options['qty']) && $options['qty'] !== '' && $options['qty'] !== null) { return self::toQty($options['qty']); } if (isset($options['num']) && $options['num'] !== '' && $options['num'] !== null) { return self::toQty($options['num']); } return self::toQty($default); } protected static function pickSheetQty(array $options, $default) { foreach (['sheet_qty', 'tldx'] as $key) { if (isset($options[$key]) && $options[$key] !== '' && $options[$key] !== null) { return self::toQty($options[$key]); } } return self::toQty($default); } /** * 按每版数量拆张。一版 = 一张流程单。 * 余数只要大于 0(哪怕只剩 1 件)也单独打一版,不满都算。 * @param float $qty * @param float $sheetQty * @return array * @throws Exception */ protected static function splitSheets($qty, $sheetQty) { if ($qty <= 0) { $qty = 1; } if ($sheetQty <= 0) { return [['no' => 1, 'total' => 1, 'qty' => $qty]]; } $full = (int)floor($qty / $sheetQty); $remain = round($qty - $full * $sheetQty, 4); $total = $full + ($remain > 0 ? 1 : 0); if ($total < 1) { $total = 1; } $max = (int)(Config::get('ps.print.max_sheets') ?: 200); if ($total > $max) { throw new Exception('拆张数量超过上限 ' . $max . ',请加大每版数量'); } $sheets = []; $left = $qty; for ($i = 1; $i <= $total; $i++) { $current = $i === $total ? $left : min($sheetQty, $left); if ($current <= 0) { $current = $sheetQty; } $sheets[] = [ 'no' => $i, 'total' => $total, 'qty' => $current, ]; $left = round($left - $current, 4); } return $sheets; } protected static function toQty($value) { if ($value === '' || $value === null) { return 0; } return round((float)$value, 4); } protected static function formatQty($value) { $text = rtrim(rtrim(number_format((float)$value, 4, '.', ''), '0'), '.'); return $text === '' ? '0' : $text; } protected static function replacePlaceholders($html, array $fill, array $processes) { $processHtml = self::processTableHtml($processes); $html = str_replace('{processes.html}', $processHtml, $html); foreach ($processes as $i => $row) { $n = $i + 1; $pairs = [ 'process.' . $n . '.name' => $row['name'] ?? '', 'process.' . $n => $row['name'] ?? '', 'process.' . $n . '.code' => $row['code'] ?? '', ]; foreach ($pairs as $key => $value) { $html = str_replace('{' . $key . '}', htmlspecialchars((string)$value, ENT_QUOTES, 'UTF-8'), $html); } } $html = preg_replace('/\{process\.\d+(?:\.[a-z_]+)?\}/', '', $html); foreach ($fill as $key => $value) { $html = str_replace('{' . $key . '}', htmlspecialchars((string)$value, ENT_QUOTES, 'UTF-8'), $html); } return $html; } protected static function defaultSheetHtml(array $plan, array $sheet) { $fill = $sheet['fill']; $e = function ($key) use ($fill) { return htmlspecialchars(isset($fill[$key]) ? $fill[$key] : '', ENT_QUOTES, 'UTF-8'); }; $html = '
'; $html .= '

生产流程单

'; $html .= '
第 ' . $e('sheet.no_text') . ' 张
'; $html .= ''; $html .= ''; $html .= ''; $html .= ''; $html .= ''; $html .= ''; $html .= ''; $html .= '
工厂' . $e('factory.name') . '工单号' . $e('workorder.no') . '
产品' . $e('product.name') . '编码' . $e('product.code') . '
规格' . $e('product.spec') . '单位' . $e('product.unit') . '
批次' . $e('workorder.batch_no') . '客户' . $e('workorder.customer') . '
本张数量' . $e('sheet.qty') . '计划总量' . htmlspecialchars(self::formatQty($plan['qty_plan']), ENT_QUOTES, 'UTF-8') . '
交货/计划日' . $e('workorder.plan_date') . '操作员' . $e('operator') . '
'; $html .= '

工序

'; $html .= self::processTableHtml($plan['processes']); if ($fill['workorder.remark'] !== '') { $html .= '

备注:' . $e('workorder.remark') . '

'; } $html .= '

打印时间:' . $e('print_time') . '

'; $html .= '
'; return $html; } protected static function processTableHtml(array $processes) { $html = ''; $html .= ''; if (!$processes) { $html .= ''; } else { foreach ($processes as $row) { $html .= ''; $html .= ''; $html .= ''; $html .= ''; $html .= ''; $html .= ''; $html .= ''; } } $html .= '
序号工序工位标准工时质检要点
暂无工序
' . htmlspecialchars((string)$row['seq'], ENT_QUOTES, 'UTF-8') . '' . htmlspecialchars((string)$row['name'], ENT_QUOTES, 'UTF-8') . '' . htmlspecialchars((string)$row['workstation'], ENT_QUOTES, 'UTF-8') . '' . htmlspecialchars((string)$row['standard_time'], ENT_QUOTES, 'UTF-8') . '' . htmlspecialchars((string)$row['qc_points'], ENT_QUOTES, 'UTF-8') . '
'; return $html; } }