| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924 |
- <?php
- namespace app\common\library\ps;
- use app\common\model\ps\Factory;
- use app\common\model\ps\Product;
- use app\common\model\ps\Template;
- use app\common\model\ps\Workorder;
- use app\common\model\User;
- use think\Config;
- use think\Db;
- use think\Exception;
- /**
- * 套打数据与分张规则。
- * 对齐现有流程单:按投料数量拆张、带工序顺序、先组数据再预览。
- */
- class PrintPlanner
- {
- /**
- * 组装一次套打所需的填充数据与分张计划
- *
- * @param string $bizType product|workorder
- * @param int $bizId
- * @param array $options sheet_qty/tldx 每张数量;qty/num 覆盖数量;processes/process_string 覆盖工序
- * @return array
- * @throws Exception
- */
- public static function prepare($bizType, $bizId, array $options = [])
- {
- if (!in_array($bizType, ['product', 'workorder'], true)) {
- throw new Exception('业务类型不正确');
- }
- $factory = Factory::get(FactoryContext::id());
- if (!$factory) {
- throw new Exception('工厂不存在');
- }
- $product = null;
- $workorder = null;
- $qtyDefault = 0;
- $sheetQtyDefault = 0;
- $bizId = (int)$bizId;
- if ($bizId > 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'];
- $storedSpec = trim((string)($workorder['spec'] ?? ''));
- if ($storedSpec === '' && $product) {
- $storedSpec = trim((string)$product['spec']);
- }
- $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'],
- 'spec' => $storedSpec,
- ];
- $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;
- if ($storedSpec !== '') {
- $product['spec'] = $storedSpec;
- }
- } elseif ($nameOverride !== '') {
- $product = ['id' => 0, 'code' => '', 'name' => $nameOverride, 'spec' => $storedSpec, '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'];
- }
- $specText = isset($header['spec'])
- ? (string)$header['spec']
- : ($product ? (string)$product['spec'] : '');
- if ($specText === '' && $product) {
- $specText = (string)$product['spec'];
- }
- $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' => $specText,
- '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'],
- 'workorder.spec' => $specText,
- '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 = '<div style="position:relative;width:' . $pageW . 'mm;height:' . $pageH
- . 'mm;margin:0;overflow:hidden;background:transparent;font-family:' . $typeface['css']
- . ';font-size:' . $typeface['px'] . 'px;font-weight:normal;line-height:1.2;">';
- if ($includeImage && $src !== '') {
- $html .= '<img src="' . $src . '" style="position:absolute;left:0;top:0;width:100%;height:100%;border:0;display:block;" alt="流程单底图" />';
- }
- 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 .= '<div style="position:absolute;left:' . $left . '%;top:' . $top
- . '%;width:' . $width . '%;height:' . $height . '%;font-size:' . $typeface['px']
- . 'px;font-weight:normal;font-style:normal;font-family:' . $typeface['css']
- . ';overflow:hidden;white-space:nowrap;text-align:' . $align
- . ';color:' . $color . ';background:transparent;">' . $content . '</div>';
- }
- $html .= '</div>';
- 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 = '<div style="position:relative;width:' . $pageW . 'mm;height:' . $pageH
- . 'mm;margin:0;background:' . $pageBg . ';overflow:hidden;font-family:SimSun,serif;color:#111;line-height:1.15;">';
- foreach ($elements as $el) {
- $html .= self::elementHtml($el);
- }
- $html .= '</div>';
- 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.spec', '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', '工单规格' => 'workorder.spec', '操作员' => '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 '<div style="' . $style . '"></div>';
- }
- $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 '<div style="' . $base . $border . $pad . $bgCss
- . 'font-family:' . $font . ';font-size:' . $size . 'px;font-weight:' . $weight
- . ';color:' . $color . ';display:flex;align-items:center;justify-content:' . $justify
- . ';overflow:hidden;white-space:nowrap;">' . $content . '</div>';
- }
- 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 = '<div style="font-family:SimSun,serif;font-size:14px;padding:12px;">';
- $html .= '<h2 style="text-align:center;margin:0 0 8px;">生产流程单</h2>';
- $html .= '<div style="text-align:right;margin-bottom:8px;">第 ' . $e('sheet.no_text') . ' 张</div>';
- $html .= '<table border="1" cellspacing="0" cellpadding="6" width="100%" style="border-collapse:collapse;">';
- $html .= '<tr><td>工厂</td><td>' . $e('factory.name') . '</td><td>工单号</td><td>' . $e('workorder.no') . '</td></tr>';
- $html .= '<tr><td>产品</td><td>' . $e('product.name') . '</td><td>编码</td><td>' . $e('product.code') . '</td></tr>';
- $html .= '<tr><td>规格</td><td>' . $e('product.spec') . '</td><td>单位</td><td>' . $e('product.unit') . '</td></tr>';
- $html .= '<tr><td>批次</td><td>' . $e('workorder.batch_no') . '</td><td>客户</td><td>' . $e('workorder.customer') . '</td></tr>';
- $html .= '<tr><td>本张数量</td><td>' . $e('sheet.qty') . '</td><td>计划总量</td><td>' . htmlspecialchars(self::formatQty($plan['qty_plan']), ENT_QUOTES, 'UTF-8') . '</td></tr>';
- $html .= '<tr><td>交货/计划日</td><td>' . $e('workorder.plan_date') . '</td><td>操作员</td><td>' . $e('operator') . '</td></tr>';
- $html .= '</table>';
- $html .= '<h3 style="margin:12px 0 6px;">工序</h3>';
- $html .= self::processTableHtml($plan['processes']);
- if ($fill['workorder.remark'] !== '') {
- $html .= '<p>备注:' . $e('workorder.remark') . '</p>';
- }
- $html .= '<p style="text-align:right;color:#666;">打印时间:' . $e('print_time') . '</p>';
- $html .= '</div>';
- return $html;
- }
- protected static function processTableHtml(array $processes)
- {
- $html = '<table border="1" cellspacing="0" cellpadding="6" width="100%" style="border-collapse:collapse;font-family:SimSun,serif;font-size:13px;">';
- $html .= '<tr><th>序号</th><th>工序</th><th>工位</th><th>标准工时</th><th>质检要点</th></tr>';
- if (!$processes) {
- $html .= '<tr><td colspan="5" style="text-align:center;">暂无工序</td></tr>';
- } else {
- foreach ($processes as $row) {
- $html .= '<tr>';
- $html .= '<td>' . htmlspecialchars((string)$row['seq'], ENT_QUOTES, 'UTF-8') . '</td>';
- $html .= '<td>' . htmlspecialchars((string)$row['name'], ENT_QUOTES, 'UTF-8') . '</td>';
- $html .= '<td>' . htmlspecialchars((string)$row['workstation'], ENT_QUOTES, 'UTF-8') . '</td>';
- $html .= '<td>' . htmlspecialchars((string)$row['standard_time'], ENT_QUOTES, 'UTF-8') . '</td>';
- $html .= '<td>' . htmlspecialchars((string)$row['qc_points'], ENT_QUOTES, 'UTF-8') . '</td>';
- $html .= '</tr>';
- }
- }
- $html .= '</table>';
- return $html;
- }
- }
|