liuhairui 1 päivä sitten
vanhempi
sitoutus
393e3d90e6

+ 283 - 170
application/admin/controller/Procuremen.php

@@ -57,7 +57,7 @@ class Procuremen extends Backend
         $eid = (int)$purchaseOrderDetailId;
         $base = $this->resolveMprocMobilePublicBaseUrl();
         if ($base === '') {
-            throw new \Exception('请在 application/extra/mproc.php 中配置外网可访问的正式域名');
+            throw new \Exception('无法生成手机端链接,请检查站点访问地址或 application/extra/mproc.php 中的 mobile_base_url');
         }
         $path = trim((string)Config::get('mproc.mobile_index_path'));
         if ($path === '') {
@@ -92,7 +92,6 @@ class Procuremen extends Backend
         if (preg_match('/^(127\.|10\.|192\.168\.|172\.(1[6-9]|2\d|3[01])\.)/', $host)) {
             return false;
         }
-        // 无点主机名(xh、erp 等)在小米/企业邮安全跳转中常报 Invalid url
         if (strpos($host, '.') === false) {
             return false;
         }
@@ -101,7 +100,7 @@ class Procuremen extends Backend
     }
 
     /**
-     * 解析用于外发邮件/短信的手机端站点根(https://域名 无末尾斜杠)
+     * 解析用于外发邮件/短信的手机端站点根
      */
     protected function resolveMprocMobilePublicBaseUrl()
     {
@@ -125,8 +124,13 @@ class Procuremen extends Backend
             }
         }
 
-        Log::write('外发邮件手机链接:未配置有效 mproc.mobile_base_url,且当前访问主机不适合作为外链,请在 application/extra/mproc.php 设置 https 正式域名', 'warning');
+        // 本地联调:127.0.0.1、内网 IP、虚拟主机等无法用「公网域名」规则时,退回当前请求根地址
+        $reqBase = rtrim($this->request->scheme() . '://' . $this->request->host(), '/');
+        if ($reqBase !== '' && preg_match('#^https?://#i', $reqBase)) {
+            return $reqBase;
+        }
 
+        Log::write('外发邮件手机链接:配置地址失效', 'warning');
         return '';
     }
 
@@ -146,47 +150,49 @@ class Procuremen extends Backend
     }
 
     /**
-     * 左侧菜单:仅「近 12 个自然月(含当月)」
+     * 获取 Redis 中  procuremen_redis
      */
-    protected function getIndexSidebarYearMonths()
+    protected function ProcuremenRedis(): array
     {
-        $ymSet = [];
-        $anchor = strtotime(date('Y-m-01'));
-        for ($i = 0; $i < 12; $i++) {
-            $ym = date('Y-m', strtotime("-{$i} month", $anchor));
-            $ymSet[$ym] = true;
-        }
-        $windowKeys = array_keys($ymSet);
-        $minYm = min($windowKeys);
-        $maxYm = max($windowKeys);
-
-        $rows = [];
         try {
-            $query = Db::table('scydgy')
-                ->alias('a')
-                ->join('mcyd b', 'b.ICYDID = a.ICYDID AND b.iStatus >= 10', 'inner')
-                ->where([
-                    'a.bwjg'    => 1,
-                    'a.iEndBz'  => 0,
-                    'a.iType'   => 0,
-                    'a.iStatus' => 10,
-                ]);
-            $this->whereMcydDputrecordValid($query, 'a');
-            $rows = $query->field("DATE_FORMAT(a.dputrecord, '%Y-%m') as ym")
-                ->group("DATE_FORMAT(a.dputrecord, '%Y-%m')")
-                ->orderRaw("DATE_FORMAT(a.dputrecord, '%Y-%m') DESC")
-                ->select();
-        } catch (\Exception $e) {
-            $rows = [];
+            $redis = redis();
+            $raw = $redis->get('procuremen_redis');
+            if ($raw === false || $raw === '') {
+                return [];
+            }
+            $decoded = json_decode($raw, true);
+            if (!is_array($decoded) || !isset($decoded['data']) || !is_array($decoded['data'])) {
+                return [];
+            }
+            return $decoded['data'];
+        } catch (\Throwable $e) {
+            return [];
         }
-        foreach ($rows as $r) {
-            $ym = isset($r['ym']) ? trim((string)$r['ym']) : '';
-            if (!preg_match('/^\d{4}-\d{2}$/', $ym)) {
+    }
+
+    /**
+     * 左侧菜单
+     */
+    protected function GetIndexYearMonths()
+    {
+        $ymSet = [];
+        //获取此方法调用缓存数据
+        foreach ($this->ProcuremenRedis() as $r) {
+            if (!is_array($r)) {
+                continue;
+            }
+            $ds = trim((string)($r['dputrecord'] ?? ''));
+            if ($ds === '' || stripos($ds, '0000-00-00') === 0) {
+                continue;
+            }
+            if (!preg_match('/^([12]\d{3})-(\d{1,2})-(\d{1,2})/', $ds, $m)) {
                 continue;
             }
-            if (strcmp($ym, $minYm) >= 0 && strcmp($ym, $maxYm) <= 0) {
-                $ymSet[$ym] = true;
+            $mo = (int)$m[2];
+            if ($mo < 1 || $mo > 12) {
+                continue;
             }
+            $ymSet[$m[1] . '-' . str_pad((string)$mo, 2, '0', STR_PAD_LEFT)] = true;
         }
 
         $ymList = array_keys($ymSet);
@@ -194,15 +200,12 @@ class Procuremen extends Backend
 
         $byYear = [];
         foreach ($ymList as $ym) {
-            if (!preg_match('/^\d{4}-\d{2}$/', $ym)) {
-                continue;
-            }
             $y = substr($ym, 0, 4);
-            $m = (int)substr($ym, 5, 2);
+            $mo = (int)substr($ym, 5, 2);
             if (!isset($byYear[$y])) {
                 $byYear[$y] = [];
             }
-            $byYear[$y][] = ['ym' => $ym, 'label' => $m . '月'];
+            $byYear[$y][] = ['ym' => $ym, 'label' => $mo . '月'];
         }
         krsort($byYear, SORT_NUMERIC);
         foreach ($byYear as $y => $items) {
@@ -226,8 +229,16 @@ class Procuremen extends Backend
         $this->request->filter(['strip_tags', 'trim']);
 
         if (!$this->request->isAjax()) {
+            $rootTrue = rtrim((string)$this->request->root(true), '/');
+            $indexPhpRoot = preg_replace('#/[^/]+\.php$#i', '/index.php', $rootTrue);
+            if ($indexPhpRoot === $rootTrue && !preg_match('#/index\.php$#i', $rootTrue)) {
+                $indexPhpRoot = $rootTrue . '/index.php';
+            }
+            $procuremenRedisApi = $indexPhpRoot . '/api/procuremen/getprocuremen';
+            $this->view->assign('procuremenRedisApi', $procuremenRedisApi);
             $this->view->assign('defaultYm', date('Y-m'));
-            $this->view->assign('sidebarYearMonths', $this->getIndexSidebarYearMonths());
+            $this->view->assign('sidebarYearMonths', $this->GetIndexYearMonths());
+
             return $this->view->fetch();
         }
 
@@ -285,7 +296,6 @@ class Procuremen extends Backend
 
             if (in_array($wffTab, ['pending', 'done', 'picked'], true)) {
                 $wantStatus = $wffTab === 'pending' ? 0 : 1;
-                $pool = [];
                 $dbRows = [];
                 try {
                     if ($wantStatus === 0) {
@@ -317,95 +327,16 @@ class Procuremen extends Backend
                         return $sid > 0 && isset($pickedSidSet[$sid]);
                     }));
                 }
-                $dStampMap = [];
-                $sidList = [];
-                foreach ($dbRows as $tmpRow) {
-                    if (is_array($tmpRow) && isset($tmpRow['scydgy_id'])) {
-                        $sid = (int)$tmpRow['scydgy_id'];
-                        if ($sid > 0) {
-                            $sidList[$sid] = true;
-                        }
-                    }
-                }
-                if ($sidList !== []) {
-                    try {
-                        $dStampMap = Db::table('scydgy')->where('ID', 'in', array_keys($sidList))->column('dStamp', 'ID');
-                    } catch (\Throwable $e) {
-                        $dStampMap = [];
-                    }
-                }
-                foreach ($dbRows as $dbRow) {
-                    $rj = null;
-                    if (!empty($dbRow['row_json'])) {
-                        $decoded = json_decode($dbRow['row_json'], true);
-                        if (is_array($decoded)) {
-                            $rj = $decoded;
-                        }
-                    }
-                    if ($rj !== null && isset($rj['ID'])) {
-                        $rid = (int)$rj['ID'];
-                        $dsFix = isset($rj['dStamp']) ? trim((string)$rj['dStamp']) : '';
-                        if (($dsFix === '' || stripos($dsFix, '0000-00-00') === 0)
-                            && isset($dStampMap[$rid]) && trim((string)$dStampMap[$rid]) !== '') {
-                            $rj['dStamp'] = $dStampMap[$rid];
-                        }
-                        if (array_key_exists('id', $dbRow)) {
-                            $rj['purchase_order_id'] = (int)$dbRow['id'];
-                        }
-                        $pool[] = $rj;
-                        continue;
-                    }
-                    $r = [];
-                    foreach ($dbRow as $k => $v) {
-                        $lk = strtolower((string)$k);
-                        if ($lk === 'id' || $lk === 'row_json') {
-                            continue;
-                        }
-                        $r[$k] = $v;
-                    }
-                    if (isset($dbRow['scydgy_id'])) {
-                        $r['ID'] = (int)$dbRow['scydgy_id'];
-                        $sid = $r['ID'];
-                        $dsOut = '';
-                        if (isset($dStampMap[$sid])) {
-                            $t = trim((string)$dStampMap[$sid]);
-                            if ($t !== '' && stripos($t, '0000-00-00') !== 0) {
-                                $dsOut = $dStampMap[$sid];
-                            }
-                        }
-                        if ($dsOut === '' && !empty($dbRow['createtime'])) {
-                            $ct = $dbRow['createtime'];
-                            if (is_numeric($ct) && (int)$ct > 946684800) {
-                                $dsOut = date('Y-m-d H:i:s', (int)$ct);
-                            } elseif (is_string($ct) && trim($ct) !== '') {
-                                $tc = trim($ct);
-                                if ($tc !== '' && stripos($tc, '0000-00-00') !== 0) {
-                                    $dsOut = $tc;
-                                }
-                            }
-                        }
-                        if ($dsOut !== '') {
-                            $r['dStamp'] = $dsOut;
-                        }
-                    }
-                    if (array_key_exists('id', $dbRow)) {
-                        $r['purchase_order_id'] = (int)$dbRow['id'];
-                    }
-                    $pool[] = $r;
-                }
+                $pool = $this->procuremenPoolFromPurchaseOrderDbRows($dbRows);
             } else {
-                $redis = redis();
-                $raw = $redis->get('procuremen_redis');
-                if ($raw === false || $raw === '') {
+                $pool = $this->ProcuremenRedis();
+                if ($pool === []) {
                     return json([
                         'total' => 0,
                         'rows'  => [],
                         'msg'   => '暂无缓存数据',
                     ]);
                 }
-                $decoded = json_decode($raw, true);
-                $pool = (is_array($decoded) && isset($decoded['data']) && is_array($decoded['data']))
-                    ? $decoded['data'] : [];
 
                 // 未发:主表已有且 status 为 0/1 才从列表隐藏;status 为空(NULL)仍可在未发展示
                 try {
@@ -668,6 +599,100 @@ class Procuremen extends Backend
         return $out;
     }
 
+    /**
+     * 与列表「已下发 / 已选中 / 已完结」同源:将 purchase_order 行还原为工序列表行(含 row_json、dStamp 补全)
+     *
+     * @return array<int, array<string, mixed>>
+     */
+    protected function procuremenPoolFromPurchaseOrderDbRows(array $dbRows): array
+    {
+        $pool = [];
+        if (!is_array($dbRows) || $dbRows === []) {
+            return $pool;
+        }
+        $dStampMap = [];
+        $sidList = [];
+        foreach ($dbRows as $tmpRow) {
+            if (is_array($tmpRow) && isset($tmpRow['scydgy_id'])) {
+                $sid = (int)$tmpRow['scydgy_id'];
+                if ($sid > 0) {
+                    $sidList[$sid] = true;
+                }
+            }
+        }
+        if ($sidList !== []) {
+            try {
+                $dStampMap = Db::table('scydgy')->where('ID', 'in', array_keys($sidList))->column('dStamp', 'ID');
+            } catch (\Throwable $e) {
+                $dStampMap = [];
+            }
+        }
+        foreach ($dbRows as $dbRow) {
+            if (!is_array($dbRow)) {
+                continue;
+            }
+            $rj = null;
+            if (!empty($dbRow['row_json'])) {
+                $decoded = json_decode($dbRow['row_json'], true);
+                if (is_array($decoded)) {
+                    $rj = $decoded;
+                }
+            }
+            if ($rj !== null && isset($rj['ID'])) {
+                $rid = (int)$rj['ID'];
+                $dsFix = isset($rj['dStamp']) ? trim((string)$rj['dStamp']) : '';
+                if (($dsFix === '' || stripos($dsFix, '0000-00-00') === 0)
+                    && isset($dStampMap[$rid]) && trim((string)$dStampMap[$rid]) !== '') {
+                    $rj['dStamp'] = $dStampMap[$rid];
+                }
+                if (array_key_exists('id', $dbRow)) {
+                    $rj['purchase_order_id'] = (int)$dbRow['id'];
+                }
+                $pool[] = $rj;
+                continue;
+            }
+            $r = [];
+            foreach ($dbRow as $k => $v) {
+                $lk = strtolower((string)$k);
+                if ($lk === 'id' || $lk === 'row_json') {
+                    continue;
+                }
+                $r[$k] = $v;
+            }
+            if (isset($dbRow['scydgy_id'])) {
+                $r['ID'] = (int)$dbRow['scydgy_id'];
+                $sid = $r['ID'];
+                $dsOut = '';
+                if (isset($dStampMap[$sid])) {
+                    $t = trim((string)$dStampMap[$sid]);
+                    if ($t !== '' && stripos($t, '0000-00-00') !== 0) {
+                        $dsOut = $dStampMap[$sid];
+                    }
+                }
+                if ($dsOut === '' && !empty($dbRow['createtime'])) {
+                    $ct = $dbRow['createtime'];
+                    if (is_numeric($ct) && (int)$ct > 946684800) {
+                        $dsOut = date('Y-m-d H:i:s', (int)$ct);
+                    } elseif (is_string($ct) && trim($ct) !== '') {
+                        $tc = trim($ct);
+                        if ($tc !== '' && stripos($tc, '0000-00-00') !== 0) {
+                            $dsOut = $tc;
+                        }
+                    }
+                }
+                if ($dsOut !== '') {
+                    $r['dStamp'] = $dsOut;
+                }
+            }
+            if (array_key_exists('id', $dbRow)) {
+                $r['purchase_order_id'] = (int)$dbRow['id'];
+            }
+            $pool[] = $r;
+        }
+
+        return $pool;
+    }
+
     /**
      * 已下发列表汇总:按工序行 ID 统计 purchase_order_detail 条数、已填金额条数、已填货期条数
      *
@@ -721,7 +746,7 @@ class Procuremen extends Backend
     }
 
     /**
-     * 列表筛选:月份、dStamp 合法性、快速搜索、Bootstrap Table filter(与原先 Redis 分支一致)
+     * 列表筛选:月份按 dputrecord(提交日期)、快速搜索、Bootstrap Table filter
      *
      * @return array
      */
@@ -755,13 +780,20 @@ class Procuremen extends Backend
             if (!is_array($r)) {
                 continue;
             }
-            $ds = isset($r['dStamp']) ? trim((string)$r['dStamp']) : '';
+            $ds = isset($r['dputrecord']) ? trim((string)$r['dputrecord']) : '';
             if ($ds === '' || stripos($ds, '0000-00-00') === 0
-                || !preg_match('/^[12]\d{3}-\d{1,2}-\d{1,2}/', $ds)) {
+                || !preg_match('/^([12]\d{3})-(\d{1,2})-(\d{1,2})/', $ds, $m)) {
                 continue;
             }
             if ($applyMonthRange) {
-                if (strcmp($ds, $monthStart) < 0 || strcmp($ds, $monthEnd) > 0) {
+                $mo = (int)$m[2];
+                if ($mo < 1 || $mo > 12) {
+                    continue;
+                }
+                $ymRow = $m[1] . '-' . str_pad((string)$mo, 2, '0', STR_PAD_LEFT);
+                $rowMonthStart = $ymRow . '-01 00:00:00';
+                $rowMonthEnd = date('Y-m-t 23:59:59', strtotime($rowMonthStart));
+                if (strcmp($rowMonthEnd, $monthStart) < 0 || strcmp($rowMonthStart, $monthEnd) > 0) {
                     continue;
                 }
             }
@@ -1813,6 +1845,19 @@ class Procuremen extends Backend
                 $this->error('请至少选择一个下发单位');
             }
 
+            $sysRq = trim((string)$this->request->post('sys_rq', ''));
+            if ($sysRq === '') {
+                $this->error('请选择截止时间');
+            }
+            $sysRqTs = strtotime($sysRq);
+            if ($sysRqTs === false || $sysRqTs <= 0) {
+                $this->error('截止时间格式无效');
+            }
+            //示例:2026-05-18 14:31:09
+            $sysRqDb = date('Y-m-d H:i:s', $sysRqTs);
+            //示例:2026-05-18 14:31
+            $sysRqNotify = date('Y-m-d H:i', $sysRqTs);
+
             $toDb = function ($value) {
                 if ($value === null) {
                     return null;
@@ -1852,6 +1897,7 @@ class Procuremen extends Backend
                     'This_quantity' => $row['This_quantity'] ?? $row['this_quantity'] ?? null,
                     'ceilingPrice'  => $row['ceilingPrice'] ?? $row['ceiling_price'] ?? null,
                     'status'        => 0,
+                    'sys_rq'        => $sysRqDb,
                 ];
 
                 if ($exists) {
@@ -1925,23 +1971,25 @@ class Procuremen extends Backend
                         您好,{$companyName}:<br><br>
                         您有新的外发加工订单待处理:<br>
                         订单号:{$row['CCYDH']}<br>
-                        印件名称:{$row['CYJMC']}<br><br>
-                        请点击下面链接,登录后可直接打开本条订单(无需再搜索):<br>
+                        印件名称:{$row['CYJMC']}<br>
+                        请在 <strong>{$sysRqNotify}</strong> 前登陆我司平台处理<br><br>
+                        您可点击链接进行查看等操作:<br>
                         <a href=\"{$mprocUrlEsc}\">{$mprocUrlEsc}</a><br><br>
-                        登录后可在手机端填写金额与交货日期。<br><br>
                         请及时查收并处理,谢谢!
                     ";
 
                     $mail->send();
 
+                    //发送短信
                     $ccydh = isset($row['CCYDH']) ? (string)$row['CCYDH'] : '';
                     $cyjmc = isset($row['CYJMC']) ? (string)$row['CYJMC'] : '';
                     $smsContent = "您好,{$companyName}:\n\n"
                         . "您有新的外发加工订单待处理:\n"
                         . "订单号:{$ccydh}\n"
-                        . "印件名称:{$cyjmc}\n\n"
-                        . "手机打开链接(登录后直达本条):\n"
-                        . $mprocUrl . "\n\n"
+                        . "印件名称:{$cyjmc}\n"
+                        . "请在 {$sysRqNotify} 前登陆我司平台处理\n\n"
+//                        . "您可点击链接进行查看等操作::\n"
+//                        . $mprocUrl . "\n\n"
                         . '请及时查收并处理,谢谢!';
                     $this->smsbao($phone, $smsContent);
                 }
@@ -2161,7 +2209,8 @@ class Procuremen extends Backend
 
 
     /**
-     * 月份外发明细导出
+     * 按月份导出:已完结且采购确认已选定供应商(明细 status=1),与列表「已完结」∩「已选中」一致。
+     * 表头固定 8 列(与历史「外发明细」模板一致):序号、传票号、传票名称、外加工单位、订法、客户名称、工序、加工金额。
      */
     public function export_month_outward()
     {
@@ -2172,35 +2221,99 @@ class Procuremen extends Backend
         }
         $monthStart = $ym . '-01 00:00:00';
         $monthEnd = date('Y-m-t 23:59:59', strtotime($monthStart));
-        $unixStart = (int)strtotime($monthStart);
-        $unixEnd = (int)strtotime($monthEnd);
 
-        $rows = [];
+        $dbRows = [];
         try {
-            $rows = Db::table('purchase_order_detail')
-                ->whereRaw(
-                    '(TRIM(CAST(createtime AS CHAR(64))) LIKE ?)'
-                    . ' OR ((createtime REGEXP \'^[0-9]+$\') AND CAST(createtime AS UNSIGNED) BETWEEN ? AND ?)',
-                    [$ym . '%', $unixStart, $unixEnd]
-                )
-                ->order('CCYDH', 'asc')
-                ->order('company_name', 'asc')
-                ->order('id', 'asc')
-                ->select();
+            $dbRows = Db::table('purchase_order')->where('status', 1)->select();
         } catch (\Throwable $e) {
-            $rows = [];
+            $dbRows = [];
+        }
+        if (!is_array($dbRows)) {
+            $dbRows = [];
         }
+        $pickedSidSet = $this->loadScydgyIdsWithPickedSupplierDetail();
+        $dbRows = array_values(array_filter($dbRows, function ($dbRow) use ($pickedSidSet) {
+            if (!is_array($dbRow)) {
+                return false;
+            }
+            $sid = (int)($dbRow['scydgy_id'] ?? 0);
+            if ($sid <= 0 && isset($dbRow['SCYDGY_ID'])) {
+                $sid = (int)$dbRow['SCYDGY_ID'];
+            }
 
-        $groups = [];
-        foreach ($rows as $r) {
-            if (!is_array($r)) {
+            return $sid > 0 && isset($pickedSidSet[$sid]);
+        }));
+
+        $pool = $this->procuremenPoolFromPurchaseOrderDbRows($dbRows);
+        $filtered = $this->filterProcuremenIndexPool($pool, $monthStart, $monthEnd, true, '', [], []);
+
+        $mainBySid = [];
+        foreach ($filtered as $rw) {
+            if (!is_array($rw)) {
                 continue;
             }
-            $k = (string)($r['CCYDH'] ?? '') . "\x1f" . (string)($r['company_name'] ?? '');
+            $rid = (int)($rw['ID'] ?? 0);
+            if ($rid > 0) {
+                $mainBySid[$rid] = $rw;
+            }
+        }
+        if ($mainBySid === []) {
+            $detailRows = [];
+        } else {
+            try {
+                $detailRows = Db::table('purchase_order_detail')
+                    ->where('scydgy_id', 'in', array_keys($mainBySid))
+                    ->where('status', 1)
+                    ->order('CCYDH', 'asc')
+                    ->order('company_name', 'asc')
+                    ->order('id', 'asc')
+                    ->select();
+            } catch (\Throwable $e) {
+                try {
+                    $detailRows = Db::table('purchase_order_detail')
+                        ->where('scydgy_id', 'in', array_keys($mainBySid))
+                        ->where('status', 1)
+                        ->order('CCYDH', 'asc')
+                        ->order('company_name', 'asc')
+                        ->order('ID', 'asc')
+                        ->select();
+                } catch (\Throwable $e2) {
+                    $detailRows = [];
+                }
+            }
+        }
+        if (!is_array($detailRows)) {
+            $detailRows = [];
+        }
+
+        $exportLines = [];
+        foreach ($detailRows as $dr) {
+            if (!is_array($dr)) {
+                continue;
+            }
+            $sid = (int)($dr['scydgy_id'] ?? $dr['SCYDGY_ID'] ?? 0);
+            if ($sid <= 0 || !isset($mainBySid[$sid])) {
+                continue;
+            }
+            $m = $mainBySid[$sid];
+            $exportLines[] = [
+                'CCYDH'        => (string)($dr['CCYDH'] ?? $m['CCYDH'] ?? ''),
+                'CYJMC'        => (string)($dr['CYJMC'] ?? $m['CYJMC'] ?? ''),
+                'company_name' => (string)($dr['company_name'] ?? ''),
+                'CDF'          => (string)($m['CDF'] ?? ''),
+                'cGzzxMc'      => (string)($m['cGzzxMc'] ?? ''),
+                'gx'           => $this->procuremenExportGxText($m),
+                'detail'       => $dr,
+            ];
+        }
+
+        $groups = [];
+        foreach ($exportLines as $line) {
+            $k = $line['CCYDH'] . "\x1f" . $line['company_name'];
             if (!isset($groups[$k])) {
                 $groups[$k] = [];
             }
-            $groups[$k][] = $r;
+            $groups[$k][] = $line;
         }
         ksort($groups, SORT_STRING);
 
@@ -2229,31 +2342,32 @@ class Procuremen extends Backend
         $sumSubtotalCounts = 0;
         $grandAmount = 0.0;
 
-        if (empty($groups)) {
+        if ($groups === []) {
             $sheet->mergeCells('A3:H3');
-            $sheet->setCellValue('A3', '(当月暂无外发明细)');
+            $sheet->setCellValue('A3', '(当月暂无符合条件的外发明细)');
             $sheet->getStyle('A3')->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER);
             $rowNum = 4;
         } else {
             foreach ($groups as $items) {
-                if (empty($items)) {
+                if ($items === []) {
                     continue;
                 }
                 $groupLineCount = count($items);
                 $subAmount = 0.0;
                 $i = 0;
-                foreach ($items as $r) {
+                foreach ($items as $line) {
                     $i++;
-                    $amt = $this->procuremenExportAmount($r);
+                    $dr = $line['detail'];
+                    $amt = $this->procuremenExportAmount($dr);
                     $subAmount += $amt;
 
                     $sheet->setCellValue('A' . $rowNum, $i);
-                    $sheet->setCellValue('B' . $rowNum, (string)($r['CCYDH'] ?? ''));
-                    $sheet->setCellValue('C' . $rowNum, (string)($r['CYJMC'] ?? ''));
-                    $sheet->setCellValue('D' . $rowNum, (string)($r['company_name'] ?? ''));
-                    $sheet->setCellValue('E' . $rowNum, (string)($r['CDF'] ?? ''));
-                    $sheet->setCellValue('F' . $rowNum, (string)($r['cGzzxMc'] ?? ''));
-                    $sheet->setCellValue('G' . $rowNum, $this->procuremenExportGxText($r));
+                    $sheet->setCellValue('B' . $rowNum, $line['CCYDH']);
+                    $sheet->setCellValue('C' . $rowNum, $line['CYJMC']);
+                    $sheet->setCellValue('D' . $rowNum, $line['company_name']);
+                    $sheet->setCellValue('E' . $rowNum, $line['CDF']);
+                    $sheet->setCellValue('F' . $rowNum, $line['cGzzxMc']);
+                    $sheet->setCellValue('G' . $rowNum, $line['gx']);
                     $sheet->setCellValue('H' . $rowNum, $amt);
                     $sheet->getStyle('H' . $rowNum)->getNumberFormat()->setFormatCode('"¥"#,##0.00');
                     $rowNum++;
@@ -2272,7 +2386,7 @@ class Procuremen extends Backend
             }
         }
 
-        $sheet->setCellValue('A' . $rowNum, '总');
+        $sheet->setCellValue('A' . $rowNum, '总');
         $sheet->setCellValue('B' . $rowNum, $sumSubtotalCounts);
         $sheet->mergeCells('G' . $rowNum . ':H' . $rowNum);
         $sheet->setCellValue('G' . $rowNum, '¥ ' . number_format($grandAmount, 2, '.', ','));
@@ -2289,7 +2403,6 @@ class Procuremen extends Backend
             ],
         ]);
         $sheet->getStyle('A3:H' . $lastRow)->getAlignment()->setVertical(Alignment::VERTICAL_CENTER);
-        // 长文案列:加宽 + 自动换行,避免导出后被截断
         if ($lastRow >= 3) {
             $sheet->getStyle('C3:C' . $lastRow)->getAlignment()->setWrapText(true)->setVertical(Alignment::VERTICAL_TOP);
             $sheet->getStyle('D3:D' . $lastRow)->getAlignment()->setWrapText(true)->setVertical(Alignment::VERTICAL_TOP);

+ 14 - 3
application/admin/view/procuremen/index.html

@@ -231,9 +231,20 @@
         display: inline-block;
         vertical-align: middle;
     }
+    /* 完结确认弹层:与操作列「完结」同为警示色主按钮 */
+    body .layui-layer-procuremen-finish .layui-layer-btn0 {
+        background-color: #f0ad4e !important;
+        border-color: #eea236 !important;
+        color: #fff !important;
+    }
+    body .layui-layer-procuremen-finish .layui-layer-btn0:hover {
+        background-color: #ec971f !important;
+        border-color: #d58512 !important;
+        color: #fff !important;
+    }
 </style>
 
-<div class="panel panel-default panel-intro" id="procuremen-layout" data-default-ym="{$defaultYm|htmlentities}">
+<div class="panel panel-default panel-intro" id="procuremen-layout" data-default-ym="{$defaultYm|htmlentities}" data-procuremen-redis-api="{$procuremenRedisApi|htmlentities}">
     {:build_heading()}
 
     <div class="panel-body">
@@ -261,8 +272,8 @@
                             </ul>
                             <div class="procuremen-table-area">
                                 <div id="toolbar" class="toolbar">
-                                    <a href="javascript:;" class="btn btn-primary btn-refresh" title="{:__('Refresh')}" ><i class="fa fa-refresh"></i> </a>
-                                    <a href="javascript:;" class="btn btn-success" id="btn-export-month-outward" title="按所选月份导出下发明细 Excel"><i class="fa fa-download"></i> 月份下发明细导出</a>
+                                    <a href="javascript:;" class="btn btn-primary btn-refresh" title="{:__('Refresh')}" >刷新 <i class="fa fa-refresh"></i> </a>
+                                    <a href="javascript:;" class="btn btn-success" id="btn-export-month-outward" title="按月份导出外发明细(8 列固定表头,已完结且已选外协)"><i class="fa fa-download"></i> 月份下发明细导出</a>
                                 </div>
                                 <table id="table"
                                        class="table table-striped table-bordered table-hover">

+ 20 - 3
application/admin/view/procuremen/outward_detail.html

@@ -54,7 +54,11 @@
         margin-top: 12px;
         padding-top: 10px;
         border-top: 1px solid #eee;
-        text-align: right;
+        display: flex;
+        justify-content: flex-end;
+        align-items: center;
+        gap: 8px;
+        flex-wrap: wrap;
     }
     .procuremen-purchase-confirm-tip {
         margin: 0 0 10px 0;
@@ -67,6 +71,18 @@
     .procuremen-purchase-confirm-tip .fa {
         margin-right: 6px;
     }
+    .btn.procuremen-btn-slate {
+        color: #fff !important;
+        background-color: #4b5573 !important;
+        border-color: #3e4659 !important;
+    }
+    .btn.procuremen-btn-slate:hover,
+    .btn.procuremen-btn-slate:focus,
+    .btn.procuremen-btn-slate:active {
+        color: #fff !important;
+        background-color: #3f485f !important;
+        border-color: #353c4c !important;
+    }
 </style>
 
 <div class="panel panel-default panel-intro outward-detail-wrap" style="border:0;box-shadow:none;" data-scydgy-id="{$scydgyId|htmlentities}" data-purchase-order-id="{$purchaseOrderId|default=0}">
@@ -74,7 +90,7 @@
     {if $showPurchaseConfirm}
     <div class="alert alert-warning procuremen-purchase-confirm-tip">
         <i class="fa fa-exclamation-triangle"></i>
-        <strong>重要提示:</strong>采购选择确认并提交后,系统将<strong>立即向各供应商下发短信</strong>;该操作<strong>不可撤回或更改</strong>,请仔细核对勾选结果后再点击「确认」。
+        <strong>重要提示:</strong>选择确认并提交后,系统将<strong>立即向各供应商下发短信</strong>;该操作<strong>不可撤回或更改</strong>,请仔细核对勾选结果后再点击「确认」。
     </div>
     {/if}
     <div class="table-responsive outward-detail-table-wrap">
@@ -162,7 +178,8 @@
     </div>
     {if $showPurchaseConfirm}
     <div class="outward-detail-foot">
-        <button type="button" class="btn btn-primary" id="btn-pod-purchase-confirm"><i class="fa fa-check"></i> 确认</button>
+        <button type="button" class="btn procuremen-btn-slate" id="btn-pod-purchase-confirm"><i class="fa fa-check"></i> 确认</button>
+        <button type="button" class="btn btn-default" id="btn-pod-purchase-close"><i class="fa fa-times"></i> 关闭</button>
     </div>
     {/if}
 </div>

+ 74 - 18
application/admin/view/procuremen/review.html

@@ -36,7 +36,7 @@
         gap: 10px;
         flex: 1 1 0%;
         min-height: 0;
-        overflow: hidden;
+        overflow: visible;
     }
     .review-split-left {
         flex: 0 0 auto;
@@ -44,7 +44,9 @@
         min-width: 0;
         padding-bottom: 10px;
         border-bottom: 1px solid #e5e5e5;
-        overflow-x: hidden;
+        overflow: visible;
+        position: relative;
+        z-index: 60;
     }
     .review-split-right {
         flex: 1 1 0%;
@@ -123,6 +125,41 @@
         font-size: 13px;
         word-break: break-word;
     }
+    .review-deadline-row {
+        display: flex;
+        flex-wrap: wrap;
+        align-items: flex-start;
+        gap: 8px 12px;
+        margin-top: 10px;
+        padding-top: 10px;
+        border-top: 1px dashed #e0e0e0;
+        width: 100%;
+        position: relative;
+        z-index: 61;
+        overflow: visible;
+    }
+    .review-deadline-row .review-field-label {
+        flex-shrink: 0;
+        margin: 0;
+        font-weight: 600;
+        font-size: 13px;
+        color: #000;
+    }
+    .review-deadline-input-wrap {
+        position: relative;
+        flex: 0 0 auto;
+        z-index: 62;
+    }
+    .review-deadline-row .review-sys-rq-input {
+        width: 240px;
+        max-width: 240px;
+        min-width: 180px;
+    }
+    /* 截止时间日历:浮在弹窗最上层,不占文档流 */
+    body.is-dialog .bootstrap-datetimepicker-widget.dropdown-menu {
+        z-index: 19999999 !important;
+        box-shadow: 0 2px 12px rgba(0, 0, 0, 0.15);
+    }
     .review-split-right .review-right-title {
         flex-shrink: 0;
         margin: 0 0 6px;
@@ -130,21 +167,32 @@
         font-size: 13px;
         color: #000;
     }
-    .review-split-right .review-notice {
+    .review-split-right .procuremen-review-submit-tip {
         flex-shrink: 0;
-        display: block;
         margin: 0 0 8px;
-        padding: 6px 8px;
-        font-size: 12px;
-        line-height: 1.5;
-        color: #777;
-        background: #f4f8fb;
-        border: 1px solid #dce8f2;
+        padding: 8px 10px;
+        border: 1px solid #eea236;
+        background: #fcf8e3;
+        color: #8a6d3b;
+        font-size: 13px;
+        line-height: 1.65;
         border-radius: 3px;
     }
-    .review-split-right .review-notice .fa {
-        margin-right: 4px;
-        color: #3c8dbc;
+    .review-split-right .procuremen-review-submit-tip .fa {
+        margin-right: 6px;
+    }
+    /* 提交:与列表审核 / 采购确认、弹层「确定」一致 */
+    .btn.procuremen-btn-slate {
+        color: #fff !important;
+        background-color: #4b5573 !important;
+        border-color: #3e4659 !important;
+    }
+    .btn.procuremen-btn-slate:hover,
+    .btn.procuremen-btn-slate:focus,
+    .btn.procuremen-btn-slate:active {
+        color: #fff !important;
+        background-color: #3f485f !important;
+        border-color: #353c4c !important;
     }
     /* 左侧分类 + 右侧列表 */
     .review-company-panel {
@@ -428,6 +476,14 @@
                     <span class="review-meta-inline" id="review-cGzzxMc"></span>
                 </div>
             </div>
+            <div class="review-deadline-row">
+                <span class="review-field-label">截止时间:<span class="text-danger">*</span></span>
+                <div class="review-deadline-input-wrap">
+                    <input type="text" id="review-sys-rq" name="sys_rq" class="form-control datetimepicker review-sys-rq-input"
+                           data-date-format="YYYY-MM-DD HH:mm:ss" data-use-current="true"
+                           placeholder="请选择年月日时分" autocomplete="off" required="required"/>
+                </div>
+            </div>
         </div>
         <div class="review-selected-summary" id="review-selected-summary" aria-live="polite">
             <div class="review-selected-head">
@@ -437,10 +493,10 @@
             <div class="review-selected-tags" id="review-selected-tags"></div>
         </div>
         <div class="review-split-right">
-            <span class="review-notice" title="操作提示">
-                <i class="fa fa-info-circle"></i>
-                提交将向<strong>已勾选</strong>单位发送<strong>邮件</strong>与<strong>手机短信</strong>通知。
-            </span>
+            <div class="alert alert-warning procuremen-review-submit-tip" role="alert">
+                <i class="fa fa-exclamation-triangle"></i>
+                <strong>重要提示:</strong>提交将向<strong>已勾选</strong>单位发送<strong>邮件</strong>与<strong>手机短信</strong>通知;该操作<strong>不可撤回或更改</strong>,请仔细核对勾选结果后再点击「确认」
+            </div>
             <div class="review-company-panel">
                 <aside class="review-category-sidebar" id="review-category-sidebar">
                     <div class="review-cat-head">本次下发分组</div>
@@ -483,8 +539,8 @@
 
     <div class="layer-footer clearfix">
         <div class="pull-right">
+            <button type="button" style="margin-right: 20px" id="btn-review-submit" class="btn procuremen-btn-slate btn-embossed"> 提交</button>
             <button type="reset"  style="margin-right: 20px" class="btn btn-default btn-embossed btn-close" onclick="Layer.closeAll();">{:__('Close')}</button>
-            <button type="button"  style="margin-right: 20px" id="btn-review-submit" class="btn btn-success btn-embossed">提交</button>
         </div>
     </div>
 </form>

+ 4 - 4
application/extra/mproc.php

@@ -6,13 +6,13 @@
  * 账号密码:与后台 admin 一致,可看全部,不可改金额/交期
  */
 return [
-    /** 邮件/短信中的手机端根地址,须为公网可解析域名(含 https),勿用内网短名如 http://xh/。示例 https://xh-erp.7in6.com */
-    'mobile_base_url'   => 'https://xh-erp.7in6.com',
-    /** 手机端首页路径(相对上项),留空为 /index.php/index/index/index(与伪静态无关时最稳) */
+    // 邮件/短信中的手机端根地址(正式环境);本地未配或校验不通过时自动用当前访问域名
+    'mobile_base_url'   => 'http://xh-erp.7in6.com',
+    // 手机端首页路径(相对上项),留空为 /index.php/index/index/index
     'mobile_index_path' => '',
     'session_days'  => 7,
     'sms_code_ttl'  => 300,
     'sms_resend_cd' => 55,
-    // 仅本地调试:与登录页输入的 6 位验证码一致时跳过「获取验证码」产生的缓存校验;上线请注释或删除本项
+    // 仅本地调试:与登录页输入的 6 位验证码一致时跳过短信缓存校验;上线请注释或删除
     'mock_sms_code' => '123456',
 ];

+ 38 - 3
public/assets/css/backend.css

@@ -1230,7 +1230,7 @@ table.table-nowrap thead > tr > th {
   min-height: 53px;
   text-align: inherit !important;
 }
-.layui-layer-fast .layui-layer-confirm {
+.layui-layer-fast .layui-layer-footer .layui-layer-confirm {
   position: absolute;
   width: 100%;
   height: 100%;
@@ -1240,7 +1240,7 @@ table.table-nowrap thead > tr > th {
   background: transparent;
   color: transparent;
 }
-.layui-layer-fast .layui-layer-confirm:focus {
+.layui-layer-fast .layui-layer-footer .layui-layer-confirm:focus {
   border: 1px solid #444c69;
   -webkit-border-radius: 2px;
   -webkit-background-clip: padding-box;
@@ -1249,7 +1249,7 @@ table.table-nowrap thead > tr > th {
   border-radius: 2px;
   background-clip: padding-box;
 }
-.layui-layer-fast .layui-layer-confirm:focus-visible {
+.layui-layer-fast .layui-layer-footer .layui-layer-confirm:focus-visible {
   outline: 0;
 }
 .layui-layer-fast .layui-layer-tab .layui-layer-title span.layui-this {
@@ -1642,4 +1642,39 @@ table.table-nowrap thead > tr > th {
 .autocontent .autocontent-caret:hover {
   color: #ccc;
 }
+
+/* 外发采购-采购确认:短信二次确认弹层(居中;勿与 footer 内 .layui-layer-confirm 透明按钮样式混淆) */
+.layui-layer.procuremen-purchase-sms-confirm,
+.layui-layer-fast.procuremen-purchase-sms-confirm.layui-layer-dialog {
+  position: fixed !important;
+  top: 50% !important;
+  left: 50% !important;
+  transform: translate(-50%, -50%) !important;
+  width: auto !important;
+  max-width: 92vw !important;
+  height: auto !important;
+  min-height: 0 !important;
+  margin: 0 !important;
+  right: auto !important;
+  bottom: auto !important;
+  background: #fff !important;
+  color: #333 !important;
+}
+/* 外发采购-审核:弹层 footer 由 iframe 内克隆到主窗口 .layui-layer-footer,子页内 style 不作用于克隆节点 */
+.layui-layer-fast .layui-layer-footer .btn.procuremen-btn-slate,
+.layui-layer-footer .btn.procuremen-btn-slate {
+  color: #fff !important;
+  background-color: #4b5573 !important;
+  border-color: #3e4659 !important;
+}
+.layui-layer-fast .layui-layer-footer .btn.procuremen-btn-slate:hover,
+.layui-layer-fast .layui-layer-footer .btn.procuremen-btn-slate:focus,
+.layui-layer-fast .layui-layer-footer .btn.procuremen-btn-slate:active,
+.layui-layer-footer .btn.procuremen-btn-slate:hover,
+.layui-layer-footer .btn.procuremen-btn-slate:focus,
+.layui-layer-footer .btn.procuremen-btn-slate:active {
+  color: #fff !important;
+  background-color: #3f485f !important;
+  border-color: #353c4c !important;
+}
 /*# sourceMappingURL=backend.css.map */

+ 237 - 16
public/assets/js/backend/procuremen.js

@@ -2,6 +2,89 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
 
     /* 列表:Controller.index 内顺序执行。审核:Controller.review 内顺序执行。无其它本文件自定义函数。 */
 
+    function procuremenLayerTop() {
+        try {
+            if (typeof top !== 'undefined' && top.Layer) {
+                return top.Layer;
+            }
+        } catch (e) {
+        }
+        try {
+            if (typeof parent !== 'undefined' && parent.Layer) {
+                return parent.Layer;
+            }
+        } catch (e) {
+        }
+        return typeof Layer !== 'undefined' ? Layer : null;
+    }
+
+    function procuremenLayerNextZIndex() {
+        var z = 19891014;
+        var $doc;
+        try {
+            $doc = (typeof top !== 'undefined' && top.jQuery) ? top.jQuery(top.document) : null;
+        } catch (e) {
+            $doc = null;
+        }
+        if (!$doc || !$doc.length) {
+            try {
+                $doc = (typeof parent !== 'undefined' && parent.jQuery) ? parent.jQuery(parent.document) : null;
+            } catch (e2) {
+                $doc = null;
+            }
+        }
+        if ($doc && $doc.length) {
+            $doc.find('.layui-layer').each(function () {
+                var zi = parseInt($(this).css('z-index'), 10) || 0;
+                if (zi > z) {
+                    z = zi;
+                }
+            });
+        }
+        return z + 10;
+    }
+
+    /** 将 layer 弹层居中到顶层窗口可视区域(采购确认短信二次确认等) */
+    function procuremenCenterLayer(layero) {
+        var $win;
+        try {
+            $win = (typeof top !== 'undefined' && top.jQuery) ? top.jQuery : null;
+        } catch (e) {
+            $win = null;
+        }
+        if (!$win) {
+            try {
+                $win = (typeof parent !== 'undefined' && parent.jQuery) ? parent.jQuery : null;
+            } catch (e2) {
+                $win = null;
+            }
+        }
+        if (!$win) {
+            $win = $;
+        }
+        var $layer = $win(layero);
+        var place = function () {
+            var w = $layer.outerWidth() || 480;
+            var h = $layer.outerHeight() || 220;
+            var $vw = $win(window);
+            var vw = $vw.width() || 0;
+            var vh = $vw.height() || 0;
+            var topPx = Math.max(12, Math.round((vh - h) / 2));
+            var leftPx = Math.max(12, Math.round((vw - w) / 2));
+            $layer.css({
+                position: 'fixed',
+                top: topPx + 'px',
+                left: leftPx + 'px',
+                right: 'auto',
+                bottom: 'auto',
+                margin: 0,
+                transform: 'none'
+            });
+        };
+        setTimeout(place, 0);
+        setTimeout(place, 80);
+    }
+
     var Controller = {
         currYm: '',
         index: function () {
@@ -101,20 +184,17 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                 var escYm = String(ymDefault == null ? '' : ymDefault).replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
                 var html = ''
                     + '<div style="padding:14px 18px 6px;">'
-                    + '<p class="text-muted" style="margin:0 0 12px;font-size:13px;line-height:1.55;">'
-                    + '默认与左侧当前月份一致,可选择导出其它月份的下发明细。'
-                    + '</p>'
                     + '<div class="form-group" style="margin-bottom:0;">'
                     + '<input type="month" id="export-outward-ym-input" class="form-control" value="' + escYm + '" style="max-width:220px;" />'
                     + '</div>'
                     + '</div>';
                 Layer.open({
                     type: 1,
-                    title: '月份发明细导出',
-                    area: ['440px', 'auto'],
+                    title: '月份发明细导出',
+                    area: ['370px', 'auto'],
                     shadeClose: true,
                     content: html,
-                    btn: ['导出明细'],
+                    btn: ['导出 Excel'],
                     yes: function (index, layero) {
                         var v = (layero.find('#export-outward-ym-input').val() || '').trim();
                         if (!/^\d{4}-\d{2}$/.test(v)) {
@@ -276,7 +356,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                 {field: 'dStamp', title: __('操作日期'), operate: 'RANGE', addclass: 'datetimerange', autocomplete: false, table: 'a', width: 165, align: 'center'},
                 {field: 'dputrecord', title: __('提交日期'), operate: 'RANGE', addclass: 'datetimerange', autocomplete: false, table: 'b', width: 170, align: 'center'},
                 {field: 'cywyxm', title: __('业务员'), operate: 'LIKE', table: 'b', width: 80, align: 'center'},
-                {field: 'operate',title: '操作',width: 260,align: 'center',fixed: 'right',
+                {field: 'operate',title: '操作',width: 170,align: 'center',fixed: 'right',
                     table: table,
                     formatter: function (value, row, index) {
                         var tab = Controller.wffTab || 'all';
@@ -443,7 +523,30 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                         if ($priceF.length) {
                             rowF = $.extend({}, rowF, {ceilingPrice: String($priceF.val()).trim()});
                         }
-                        Layer.confirm('确认后仅将本单标记为已完结。', function (idx) {
+                        var escHtmlFinish = function (s) {
+                            return String(s == null ? '' : s)
+                                .replace(/&/g, '&amp;')
+                                .replace(/</g, '&lt;')
+                                .replace(/>/g, '&gt;')
+                                .replace(/"/g, '&quot;');
+                        };
+                        var ccydhF = String(rowF.CCYDH != null ? rowF.CCYDH : '').trim();
+                        var cyjmcF = String(rowF.CYJMC != null ? rowF.CYJMC : '').trim();
+                        var finishMsg = '<div style="text-align:left;line-height:1.65;font-size:13px;padding:2px 0;">'
+                            + '<p style="margin:0 0 10px 0;">即将对以下<strong>订单</strong>标记为<strong>已完结</strong>:</p>'
+                            + '<div style="margin-bottom:12px;padding:8px 10px;background:#f9f9f9;border:1px solid #e5e5e5;border-radius:3px;">'
+                            + '<div><span style="color:#888;">订单号:</span><strong>' + (ccydhF !== '' ? escHtmlFinish(ccydhF) : '—') + '</strong></div>'
+                            + '<div style="margin-top:6px;"><span style="color:#888;">印件名称:</span>' + (cyjmcF !== '' ? escHtmlFinish(cyjmcF) : '—') + '</div>'
+                            + '</div>'
+                            + '<p style="margin:0;color:#a94442;"><strong>提示:</strong>请确认核对后、。<strong>点击「确定」后不可撤回或更改。</strong></p>'
+                            + '</div>';
+                        Layer.confirm(finishMsg, {
+                            title: '完结确认',
+                            area: ['520px', 'auto'],
+                            icon: 3,
+                            btn: ['确定', '取消'],
+                            skin: 'layui-layer-procuremen-finish'
+                        }, function (idx) {
                             Layer.close(idx);
                             Fast.api.ajax({
                                 url: 'procuremen/completeDirectly',
@@ -599,12 +702,42 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                 tableRoot.addEventListener('click', Controller._procuremenOpTableClick, true);
             }
 
-            $layout.find('.procuremen-main').off('click.procuremenTbRefresh').on('click.procuremenTbRefresh', '#procuremen-toolbar-host .btn-refresh', function (e) {
+            $layout.find('.procuremen-main').off('click.procuremenTbRefresh').on('click.procuremenTbRefresh', '#procuremen-toolbar-host .btn-refresh, .procuremen-table-area #toolbar .btn-refresh', function (e) {
                 e.preventDefault();
-                table.bootstrapTable('refresh');
+                var $spinFa = $('#procuremen-toolbar-host .btn-refresh .fa, .procuremen-table-area #toolbar .btn-refresh .fa');
+                $spinFa.addClass('fa-spin');
+                var apiBase = ($layout.attr('data-procuremen-redis-api') || '').toString().trim();
+                if (apiBase) {
+                    var sep = apiBase.indexOf('?') > -1 ? '&' : '?';
+                    var refreshUrl = apiBase + sep + 'refresh=1';
+                    $.ajax({
+                        url: refreshUrl,
+                        type: 'GET',
+                        dataType: 'json',
+                        timeout: 120000
+                    }).done(function (ret) {
+                        
+                        if (ret && (ret.code === 200 || ret.code === '200')) {
+                            if (typeof Toastr !== 'undefined') {
+                                console.log('已刷新');
+                                // console.log(ret);
+                                // Toastr.success('已刷新');
+                            }
+                        } else if (typeof Toastr !== 'undefined') {
+                            Toastr.warning((ret && ret.msg) ? String(ret.msg) : '刷新接口返回异常');
+                        }
+                    }).fail(function (xhr) {
+                        if (typeof Toastr !== 'undefined') {
+                            Toastr.error(xhr && xhr.statusText ? ('刷新失败:' + xhr.statusText) : '刷新缓存请求失败');
+                        }
+                    }).always(function () {
+                        table.bootstrapTable('refresh');
+                    });
+                } else {
+                    table.bootstrapTable('refresh');
+                }
             });
             table.on('refresh.bs.table', function () {
-                $('#procuremen-toolbar-host .btn-refresh .fa').addClass('fa-spin');
                 setTimeout(function () {
                     var $host2 = $('#procuremen-toolbar-host');
                     var $bt2 = table.closest('.bootstrap-table');
@@ -753,14 +886,13 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                     + '<div style="text-align:left;line-height:1.75;font-size:13px;">'
                     + '<p style="margin:0 0 10px 0;">提交后将<strong>立即发送短信</strong>,且<strong>不可撤回或更改</strong>。请确认以下通知:</p>'
                     + '<ul style="margin:0;padding-left:1.2em;">'
-                    + '<li style="margin-bottom:6px;"><strong>已选中 1 条</strong>:采购确认结果视为「<strong>通过</strong>」,将向 <strong>' + escHtml(okName) + '</strong> 发送「已通过」短信;</li>'
-                    + '<li><strong>未选中 ' + nUn + ' 条</strong>:视为「<strong>未通过</strong>」,将向对应供应商发送「未通过」短信。</li>'
+                    + '<li style="margin-bottom:6px;"><strong>已选中 1 条</strong>:将向 <strong>' + escHtml(okName) + '</strong> 发送「已通过」短信;</li>'
+                    + '<li><strong>未选中 ' + nUn + ' 条</strong>:将向 <strong>' + (nUn ? escHtml(unListText) : '对应供应商') + '</strong> 发送「未通过」短信。</li>'
                     + '</ul>'
-                    + (nUn ? ('<p style="margin:8px 0 0 0;color:#888;font-size:12px;">未选中涉及:' + escHtml(unListText) + '</p>') : '')
                     + '<p style="margin:12px 0 0 0;"><strong>是否确认提交?</strong></p>'
                     + '</div>';
 
-                var Lr = (typeof parent !== 'undefined' && parent.Layer) ? parent.Layer : (typeof Layer !== 'undefined' ? Layer : null);
+                var Lr = procuremenLayerTop();
                 if (!Lr || typeof Lr.confirm !== 'function') {
                     if (typeof Toastr !== 'undefined') {
                         Toastr.error('弹层组件未就绪,请刷新后重试');
@@ -772,7 +904,18 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                     icon: 3,
                     title: '采购确认 — 短信通知',
                     area: ['480px', 'auto'],
-                    btn: ['确定提交', '取消']
+                    offset: 'auto',
+                    btn: ['确定提交', '取消'],
+                    fixed: true,
+                    shade: 0.35,
+                    zIndex: procuremenLayerNextZIndex(),
+                    skin: 'layui-layer-fast procuremen-purchase-sms-confirm',
+                    success: function (layero) {
+                        procuremenCenterLayer(layero);
+                    },
+                    resizing: function (layero) {
+                        procuremenCenterLayer(layero);
+                    }
                 }, function (idx) {
                     Lr.close(idx);
                     FastRef.api.ajax({
@@ -806,6 +949,18 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                     });
                 });
             });
+            $wrap.on('click.procuremenPurchaseConfirm', '#btn-pod-purchase-close', function () {
+                if (typeof parent !== 'undefined' && parent.Layer) {
+                    var ixClose = parent.Layer.getFrameIndex(window.name);
+                    if (typeof ixClose !== 'undefined' && ixClose !== null) {
+                        parent.Layer.close(ixClose);
+                        return;
+                    }
+                }
+                if (typeof Layer !== 'undefined' && typeof Layer.closeAll === 'function') {
+                    Layer.closeAll();
+                }
+            });
         },
 
         details: function () {
@@ -813,6 +968,65 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
         },
 
         review: function () {
+            Controller.api.bindevent();
+            require(['bootstrap-datetimepicker'], function () {
+                var $el = $('#review-sys-rq');
+                if (!$el.length) {
+                    return;
+                }
+                var placePickerBesideInput = function () {
+                    var picker = $el.data('DateTimePicker');
+                    if (!picker || !picker.widget) {
+                        return;
+                    }
+                    var $w = picker.widget;
+                    var rect = $el[0].getBoundingClientRect();
+                    var gap = 12;
+                    var top = rect.top;
+                    var left = rect.right + gap;
+                    var widgetW = $w.outerWidth() || 300;
+                    var widgetH = $w.outerHeight() || 320;
+                    var vw = window.innerWidth || document.documentElement.clientWidth;
+                    var vh = window.innerHeight || document.documentElement.clientHeight;
+                    if (left + widgetW > vw - 8) {
+                        left = Math.max(8, rect.left);
+                        top = rect.bottom + 6;
+                    }
+                    if (top + widgetH > vh - 8) {
+                        top = Math.max(8, rect.top - widgetH - 6);
+                    }
+                    $w.appendTo(document.body).css({
+                        position: 'fixed',
+                        top: top + 'px',
+                        left: left + 'px',
+                        right: 'auto',
+                        bottom: 'auto',
+                        display: 'block',
+                        margin: 0
+                    });
+                };
+                var bindPickerFloat = function (attempt) {
+                    if ($el.data('DateTimePicker')) {
+                        $el.off('dp.show.reviewSysRq dp.update.reviewSysRq focus.reviewSysRq click.reviewSysRq')
+                            .on('dp.show.reviewSysRq dp.update.reviewSysRq', function () {
+                                setTimeout(placePickerBesideInput, 0);
+                            })
+                            .on('focus.reviewSysRq click.reviewSysRq', function () {
+                                var picker = $el.data('DateTimePicker');
+                                if (picker) {
+                                    picker.show();
+                                }
+                            });
+                        return;
+                    }
+                    if (attempt < 30) {
+                        setTimeout(function () {
+                            bindPickerFloat(attempt + 1);
+                        }, 50);
+                    }
+                };
+                bindPickerFloat(0);
+            });
             var ids = window.Fast && Fast.api ? Fast.api.query('ids') : null;
             if (ids === null || ids === '') {
                 Toastr.error('请刷新页面后重试');
@@ -1151,6 +1365,12 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                     Toastr.warning('请至少选择一个下发单位');
                     return;
                 }
+                var sysRq = ($('#review-sys-rq').val() || '').trim();
+                if (!sysRq) {
+                    Toastr.warning('请选择截止时间');
+                    $('#review-sys-rq').focus();
+                    return;
+                }
                 var cons;
                 try {
                     cons = JSON.parse($('#c-row-json').val() || '{}');
@@ -1164,6 +1384,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                         __token__: $('input[name=\'__token__\']').val(),
                         row_json: JSON.stringify(cons),
                         companies_json: JSON.stringify(selected),
+                        sys_rq: sysRq
                     }
                 }, function (data, ret) {
                     var msg = (ret && ret.msg) ? ret.msg : '操作成功';