| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908 |
- <?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'];
- $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 = '<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.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 '<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;
- }
- }
|