m0_70156489 13 ساعت پیش
والد
کامیت
b0fd8f6fc1

+ 6 - 5
application/admin/controller/Procuremen.php

@@ -3288,11 +3288,12 @@ class Procuremen extends Backend
                 continue;
             }
             $out[] = [
-                'id'         => $one['id'],
-                'username'   => $one['username'],
-                'nickname'   => $one['nickname'],
-                'group_name' => $groupNameMap[$one['id']] ?? '',
-                'mobile'     => mask_phone($one['mobile']),
+                'id'           => $one['id'],
+                'username'     => $one['username'],
+                'nickname'     => $one['nickname'],
+                'group_name'   => $groupNameMap[$one['id']] ?? '',
+                'mobile'       => mask_phone($one['mobile']),
+                'mobile_plain' => (string)$one['mobile'],
             ];
         }
 

+ 13 - 5
application/admin/controller/Procuremenarchive.php

@@ -8,7 +8,7 @@ use app\common\library\ProcuremenTime;
 use think\Db;
 
 /**
- * 协助采购 — 历史存证档案查询(已完结工序 + 操作记录详情)
+ * 协助采购 — 历史存证档案查询(审批通过即可查;含操作记录详情)
  *
  * @icon fa fa-archive
  */
@@ -87,10 +87,18 @@ class Procuremenarchive extends Backend
         return false;
     }
 
-    /** purchase_order.status 为已完结(兼容 varchar / 数字 / 首尾空格) */
-    protected function applyPurchaseOrderCompletedWhere($query): void
+    /**
+     * 历史存证范围:审批通过(wflow=已审核)即可显示,不要求入库合格/已完结
+     * (兼容旧数据仅有 status=已完结)
+     */
+    protected function applyPurchaseOrderArchiveWhere($query): void
     {
-        $query->whereRaw(ProcuremenStatus::sqlPoCompleted('status'));
+        $quoted = [];
+        foreach (ProcuremenStatus::wflowApprovedValues() as $v) {
+            $quoted[] = "'" . str_replace("'", "''", (string)$v) . "'";
+        }
+        $wflowSql = 'TRIM(CAST(`wflow_status` AS CHAR)) IN (' . implode(',', $quoted) . ')';
+        $query->whereRaw('(' . $wflowSql . ' OR ' . ProcuremenStatus::sqlPoCompleted('status') . ')');
     }
 
     public function index()
@@ -103,7 +111,7 @@ class Procuremenarchive extends Backend
             $offset = max(0, (int)$offset);
 
             $applyFilters = function ($query) use ($where) {
-                $this->applyPurchaseOrderCompletedWhere($query);
+                $this->applyPurchaseOrderArchiveWhere($query);
                 if (!empty($where)) {
                     $query->where($where);
                 }

+ 102 - 39
application/admin/controller/Supplierservicescore.php

@@ -168,8 +168,8 @@ class Supplierservicescore extends Backend
     }
 
     /**
-     * 导出供应商评审表
-     * 列:序号、供应商、最终得分、评分等级
+     * 导出供应商评审表(图二样式)
+     * 列:序号、供应商名称、质量、交货、价格、增值服务、总分、评审等级
      */
     public function export()
     {
@@ -186,7 +186,7 @@ class Supplierservicescore extends Backend
             $this->error('查询月份无效');
         }
 
-        $list = $this->loadMonthlySupplierScoreList($ym, '');
+        $list = $this->loadMonthlySupplierScoreList($ym, '', false);
 
         $title = $year . '年' . $month . '月外协加工供应商评审表';
         $spreadsheet = new Spreadsheet();
@@ -208,23 +208,24 @@ class Supplierservicescore extends Backend
             'wrapText'   => true,
         ];
 
-        $sheet->mergeCells('A1:D1');
+        $sheet->mergeCells('A1:H1');
         $sheet->setCellValue('A1', $title);
         $sheet->getRowDimension(1)->setRowHeight(36);
         $sheet->getStyle('A1')->getFont()->setName($fontName)->setBold(true)->setSize(20);
         $sheet->getStyle('A1')->getAlignment()->applyFromArray($centerAlign);
 
-        $sheet->setCellValue('A2', '序号');
-        $sheet->setCellValue('B2', '供应商');
-        $sheet->setCellValue('C2', '最终得分');
-        $sheet->setCellValue('D2', '评分等级');
+        $headers = ['序号', '供应商名称', '质量', '交货', '价格', '增值服务', '总分', '评审等级'];
+        foreach ($headers as $idx => $label) {
+            $col = chr(ord('A') + $idx);
+            $sheet->setCellValue($col . '2', $label);
+        }
         $sheet->getRowDimension(2)->setRowHeight(26);
-        $sheet->getStyle('A2:D2')->getFont()->setName($fontName)->setBold(true)->setSize(14);
-        $sheet->getStyle('A2:D2')->getAlignment()->applyFromArray($centerAlign);
+        $sheet->getStyle('A2:H2')->getFont()->setName($fontName)->setBold(true)->setSize(14);
+        $sheet->getStyle('A2:H2')->getAlignment()->applyFromArray($centerAlign);
 
         $rowNum = 3;
         if ($list === []) {
-            $sheet->mergeCells('A3:D3');
+            $sheet->mergeCells('A3:H3');
             $sheet->setCellValue('A3', '(该月暂无供应商评审记录)');
             $sheet->getRowDimension(3)->setRowHeight(26);
             $sheet->getStyle('A3')->getFont()->setName($fontName)->setSize(12);
@@ -235,8 +236,12 @@ class Supplierservicescore extends Backend
             foreach ($list as $item) {
                 $sheet->setCellValue('A' . $rowNum, $i);
                 $sheet->setCellValue('B' . $rowNum, (string)($item['company_name'] ?? ''));
-                $sheet->setCellValue('C' . $rowNum, (string)($item['final_score_text'] ?? ''));
-                $sheet->setCellValue('D' . $rowNum, (string)($item['score_grade'] ?? ''));
+                $sheet->setCellValue('C' . $rowNum, (string)($item['quality_score_text'] ?? ''));
+                $sheet->setCellValue('D' . $rowNum, (string)($item['delivery_score_text'] ?? ''));
+                $sheet->setCellValue('E' . $rowNum, (string)($item['price_score_text'] ?? ''));
+                $sheet->setCellValue('F' . $rowNum, (string)($item['value_added_score_text'] ?? ''));
+                $sheet->setCellValue('G' . $rowNum, (string)($item['final_score_text'] ?? ''));
+                $sheet->setCellValue('H' . $rowNum, (string)($item['score_grade'] ?? ''));
                 $sheet->getRowDimension($rowNum)->setRowHeight(24);
                 $rowNum++;
                 $i++;
@@ -244,19 +249,27 @@ class Supplierservicescore extends Backend
         }
 
         $lastRow = max(2, $rowNum - 1);
-        $sheet->getStyle('A1:D' . $lastRow)->applyFromArray($borderStyle);
-        $sheet->getStyle('A3:D' . $lastRow)->getFont()->setName($fontName)->setSize(12);
-        $sheet->getStyle('A3:D' . $lastRow)->getAlignment()->applyFromArray($centerAlign);
-        $sheet->getColumnDimension('A')->setWidth(12);
+        $sheet->getStyle('A1:H' . $lastRow)->applyFromArray($borderStyle);
+        $sheet->getStyle('A3:H' . $lastRow)->getFont()->setName($fontName)->setSize(12);
+        $sheet->getStyle('A3:H' . $lastRow)->getAlignment()->applyFromArray($centerAlign);
+        $sheet->getColumnDimension('A')->setWidth(10);
         $sheet->getColumnDimension('B')->setWidth(48);
-        $sheet->getColumnDimension('C')->setWidth(14);
+        $sheet->getColumnDimension('C')->setWidth(12);
         $sheet->getColumnDimension('D')->setWidth(12);
+        $sheet->getColumnDimension('E')->setWidth(12);
+        $sheet->getColumnDimension('F')->setWidth(12);
+        $sheet->getColumnDimension('G')->setWidth(12);
+        $sheet->getColumnDimension('H')->setWidth(12);
         $sheet->getPageSetup()->setFitToPage(true)->setFitToWidth(1)->setFitToHeight(0);
 
-        $filename = $title . '.xlsx';
+        $filenameCn = $title . '.xlsx';
+        $filenameAscii = 'supplier_review_' . $ym . '.xlsx';
         header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
-        header('Content-Disposition: attachment;filename="' . rawurlencode($filename) . '"');
+        header(
+            'Content-Disposition: attachment; filename="' . $filenameAscii . '"; filename*=UTF-8\'\'' . rawurlencode($filenameCn)
+        );
         header('Cache-Control: max-age=0');
+        header('X-Content-Type-Options: nosniff');
         $writer = new Xlsx($spreadsheet);
         $writer->save('php://output');
         $spreadsheet->disconnectWorksheets();
@@ -267,16 +280,17 @@ class Supplierservicescore extends Backend
     /**
      * 月度评审记录列表(仅展示,最终得分可人工填写)
      *
+     * @param bool $resync 是否先按规则重算(导出时 false,避免卡顿)
      * @return array<int, array<string, mixed>>
      */
-    protected function loadMonthlySupplierScoreList(string $ym, string $keyword = ''): array
+    protected function loadMonthlySupplierScoreList(string $ym, string $keyword = '', bool $resync = true): array
     {
         $ym = trim($ym);
         if (!preg_match('/^\d{4}-\d{2}$/', $ym)) {
             return [];
         }
         ProcuremenSupplierScore::ensureSchema();
-        $monthlyMap = ProcuremenSupplierScore::loadMonthlyMapByYm($ym);
+        $monthlyMap = ProcuremenSupplierScore::loadMonthlyMapByYm($ym, $resync);
 
         $keyword = trim($keyword);
         $list = [];
@@ -476,8 +490,7 @@ class Supplierservicescore extends Backend
      * @return array<int, array<string, mixed>>
      */
     /**
-     * 本月订单明细:与质量得分同源 —— 该供应商本月「质量评分」订单
-     * (不再依赖招标评分表,也不强制已完结;与 calcQualityScoreFromInbound 一致)
+     * 本月订单明细:与质量得分同源 —— 该供应商「完结时间」落在本月且已质量评分的订单
      *
      * @return array<int, array<string, mixed>>
      */
@@ -490,33 +503,71 @@ class Supplierservicescore extends Backend
         }
         ProcuremenSupplierScore::ensureSchema();
 
-        // 1) 与质量分同一来源:入库/质量评分表
+        // 1) 入库/质量评分表(先取该供应商全部,再按完结月过滤,与月度计分一致)
         try {
-            $scoreRows = \think\Db::table('purchase_order_inbound_score')
+            $scoreRowsRaw = \think\Db::table('purchase_order_inbound_score')
                 ->where('company_name', $companyName)
-                ->where(function ($q) use ($ym) {
-                    $q->where('updatetime', 'like', $ym . '%')
-                        ->whereOr('createtime', 'like', $ym . '%');
-                })
-                ->field('scydgy_id,ccydh,cyjmc,cgymc,result,delivery_status,customer_complaint,order_interrupt,remark,createtime,updatetime')
+                ->field('scydgy_id,ccydh,cyjmc,cgymc,result,delivery_status,delivery_date,customer_complaint,complaint_remark,order_interrupt,remark,createtime,updatetime')
                 ->order('id', 'desc')
                 ->select();
         } catch (\Throwable $e) {
             try {
-                $scoreRows = \think\Db::table('purchase_order_inbound_score')
+                $scoreRowsRaw = \think\Db::table('purchase_order_inbound_score')
                     ->where('company_name', $companyName)
-                    ->where(function ($q) use ($ym) {
-                        $q->where('updatetime', 'like', $ym . '%')
-                            ->whereOr('createtime', 'like', $ym . '%');
-                    })
-                    ->field('scydgy_id,ccydh,cyjmc,cgymc,result,remark,createtime,updatetime')
+                    ->field('scydgy_id,ccydh,cyjmc,cgymc,result,delivery_status,customer_complaint,order_interrupt,remark,createtime,updatetime')
                     ->order('id', 'desc')
                     ->select();
             } catch (\Throwable $e2) {
-                $scoreRows = [];
+                try {
+                    $scoreRowsRaw = \think\Db::table('purchase_order_inbound_score')
+                        ->where('company_name', $companyName)
+                        ->field('scydgy_id,ccydh,cyjmc,cgymc,result,remark,createtime,updatetime')
+                        ->order('id', 'desc')
+                        ->select();
+                } catch (\Throwable $e3) {
+                    $scoreRowsRaw = [];
+                }
+            }
+        }
+        if (!is_array($scoreRowsRaw) || $scoreRowsRaw === []) {
+            return [];
+        }
+        $sidSet = [];
+        $inboundTimeBySid = [];
+        foreach ($scoreRowsRaw as $r) {
+            if (!is_array($r)) {
+                continue;
+            }
+            $sid = (int)($r['scydgy_id'] ?? 0);
+            if ($sid <= 0) {
+                continue;
             }
+            $sidSet[$sid] = true;
+            $inboundTimeBySid[$sid] = [
+                'createtime' => $r['createtime'] ?? null,
+                'updatetime' => $r['updatetime'] ?? null,
+            ];
         }
-        if (!is_array($scoreRows) || $scoreRows === []) {
+        $completedYmMap = ProcuremenSupplierScore::loadCompletedYmMapByScydgyIds(array_keys($sidSet), $inboundTimeBySid);
+        $scoreRows = [];
+        foreach ($scoreRowsRaw as $r) {
+            if (!is_array($r)) {
+                continue;
+            }
+            $sid = (int)($r['scydgy_id'] ?? 0);
+            $rowYm = $sid > 0 ? (string)($completedYmMap[$sid] ?? '') : '';
+            if ($rowYm === '' && $sid <= 0) {
+                $t1 = \app\common\library\ProcuremenTime::parseToTimestamp($r['updatetime'] ?? null);
+                $t2 = \app\common\library\ProcuremenTime::parseToTimestamp($r['createtime'] ?? null);
+                $ts = max($t1, $t2);
+                $rowYm = $ts > 946684800 ? date('Y-m', $ts) : '';
+            }
+            if ($rowYm !== $ym) {
+                continue;
+            }
+            $scoreRows[] = $r;
+        }
+        if ($scoreRows === []) {
             return [];
         }
 
@@ -553,7 +604,9 @@ class Supplierservicescore extends Backend
                     'CGYMC_list'         => [],
                     'inbound_result'     => $result,
                     'delivery_status'    => $ds,
+                    'delivery_date'      => '',
                     'customer_complaint' => (int)($r['customer_complaint'] ?? 0) === 1 ? 1 : 0,
+                    'complaint_remark'   => '',
                     'order_interrupt'    => (int)($r['order_interrupt'] ?? 0) === 1 ? 1 : 0,
                     'inbound_remark'     => trim((string)($r['remark'] ?? '')),
                     'score_time'         => $scoreTime,
@@ -588,9 +641,17 @@ class Supplierservicescore extends Backend
                     $g['delivery_status'] = $ds;
                 }
             }
+            $dd = trim((string)($r['delivery_date'] ?? ''));
+            if ($dd !== '' && preg_match('/^(\d{4}-\d{2}-\d{2})/', $dd, $mdd) && $g['delivery_date'] === '') {
+                $g['delivery_date'] = $mdd[1];
+            }
             if ((int)($r['customer_complaint'] ?? 0) === 1) {
                 $g['customer_complaint'] = 1;
             }
+            $crm = trim((string)($r['complaint_remark'] ?? ''));
+            if ($crm !== '' && $g['complaint_remark'] === '') {
+                $g['complaint_remark'] = $crm;
+            }
             if ((int)($r['order_interrupt'] ?? 0) === 1) {
                 $g['order_interrupt'] = 1;
             }
@@ -769,7 +830,9 @@ class Supplierservicescore extends Backend
                 'show_bid_result'     => $showBid ? 1 : 0,
                 'inbound_result'      => (string)($g['inbound_result'] ?? ''),
                 'delivery_status'     => (string)($g['delivery_status'] ?? ''),
+                'delivery_date'       => (string)($g['delivery_date'] ?? ''),
                 'customer_complaint'  => (int)($g['customer_complaint'] ?? 0),
+                'complaint_remark'    => (string)($g['complaint_remark'] ?? ''),
                 'order_interrupt'     => (int)($g['order_interrupt'] ?? 0),
                 'inbound_remark'      => (string)($g['inbound_remark'] ?? ''),
             ];

+ 78 - 15
application/admin/view/procuremen/audit_issue.html

@@ -64,7 +64,7 @@
         vertical-align: middle;
     }
     .audit-issue-wrap .audit-table-wrap {
-        /* 不在表格区域内部滚动,由弹窗页面整体滚动 */
+        /* 不在表格区域内部纵向滚动,由弹窗页面整体滚动 */
         max-height: none;
         overflow-x: auto;
         overflow-y: visible;
@@ -76,17 +76,25 @@
         font-size: 12px;
         table-layout: fixed;
         width: 100%;
-        min-width: 980px;
-    }
-    .audit-issue-wrap .audit-process-table col:nth-child(3) {
-        width: 130px;
-    }
-    .audit-issue-wrap .audit-process-table col:nth-child(4) {
-        width: 80px;
+        min-width: 0;
+        max-width: 100%;
     }
     .audit-issue-wrap .audit-process-table td.audit-process-name {
         white-space: nowrap;
     }
+    .audit-issue-wrap .audit-process-table th.audit-col-name,
+    .audit-issue-wrap .audit-process-table td.audit-col-name {
+        width: 26%;
+        white-space: normal;
+        word-break: break-word;
+    }
+    .audit-issue-wrap .audit-process-table th.audit-col-remark,
+    .audit-issue-wrap .audit-process-table td.audit-col-remark {
+        width: 120px;
+        max-width: 120px;
+        white-space: normal;
+        word-break: break-word;
+    }
     .audit-issue-wrap .audit-process-table th {
         background: #f5f5f5;
         white-space: nowrap;
@@ -103,6 +111,9 @@
         padding: 6px 8px;
         vertical-align: middle;
         word-wrap: break-word;
+        word-break: break-word;
+        white-space: normal;
+        line-height: 1.45;
         border: 1px solid #ddd !important;
     }
     .audit-issue-wrap .audit-table {
@@ -304,6 +315,7 @@
     .audit-bid-open-modal {
         padding: 16px 18px 8px;
         font-size: 13px;
+        overflow: visible;
     }
     .audit-bid-open-modal .bid-open-tip {
         margin: 0 0 12px;
@@ -320,17 +332,68 @@
         flex-wrap: wrap;
         gap: 8px;
         margin-bottom: 10px;
+        position: relative;
     }
     .audit-bid-open-modal .bid-open-row label {
         width: 72px;
         margin: 0;
         font-weight: 600;
         color: #333;
+        flex-shrink: 0;
     }
-    .audit-bid-open-modal .bid-open-row select {
+    .audit-bid-open-modal .bid-open-picker {
+        position: relative;
         width: 260px;
+        flex-shrink: 0;
+    }
+    .audit-bid-open-modal .bid-open-picker .bid-open-search {
+        width: 100%;
         height: 32px;
     }
+    .audit-bid-open-modal .bid-open-dropdown {
+        display: none;
+        position: absolute;
+        left: 0;
+        right: 0;
+        top: 100%;
+        margin-top: 2px;
+        z-index: 19891020;
+        background: #fff;
+        border: 1px solid #d2d6de;
+        border-radius: 3px;
+        box-shadow: 0 4px 12px rgba(0,0,0,.12);
+    }
+    .audit-bid-open-modal .bid-open-dropdown.open {
+        display: block;
+    }
+    .audit-bid-open-modal .bid-open-dropdown-list {
+        max-height: 180px;
+        overflow-y: auto;
+        overflow-x: hidden;
+    }
+    .audit-bid-open-modal .bid-open-option {
+        display: block;
+        width: 100%;
+        padding: 7px 10px;
+        border: 0;
+        background: #fff;
+        text-align: left;
+        font-size: 12px;
+        line-height: 1.45;
+        color: #333;
+        cursor: pointer;
+    }
+    .audit-bid-open-modal .bid-open-option:hover,
+    .audit-bid-open-modal .bid-open-option.active {
+        background: #f0f7fb;
+        color: #3c8dbc;
+    }
+    .audit-bid-open-modal .bid-open-empty {
+        padding: 10px;
+        color: #999;
+        text-align: center;
+        font-size: 12px;
+    }
     .audit-bid-open-modal .bid-open-row .bid-open-code {
         width: 110px;
         height: 32px;
@@ -350,23 +413,23 @@
             <colgroup>
                 <col style="width:100px"/>
                 <col style="width:26%"/>
-                <col style="width:130px"/>
+                <col style="width:120px"/>
                 <col style="width:80px"/>
                 <col style="width:72px"/>
                 <col style="width:72px"/>
                 <col style="width:80px"/>
-                <col/>
+                <col style="width:120px"/>
             </colgroup>
             <thead>
             <tr>
                 <th class="text-center">订单号</th>
-                <th>印件名称</th>
+                <th class="audit-col-name">印件名称</th>
                 <th>工序名称</th>
                 <th class="text-center">单位</th>
                 <th class="text-center">本次数量</th>
                 <th class="text-center">最高限价</th>
                 <th class="text-center">订法</th>
-                <th>备注</th>
+                <th class="audit-col-remark">备注</th>
             </tr>
             </thead>
             <tbody>
@@ -374,14 +437,14 @@
             <tr>
                 {if $pr.show_order_cells}
                 <td class="text-center"{if condition="$pr.order_rowspan gt 1"} rowspan="{$pr.order_rowspan}"{/if}>{$pr.CCYDH|default=''}</td>
-                <td{if condition="$pr.order_rowspan gt 1"} rowspan="{$pr.order_rowspan}"{/if}>{$pr.CYJMC|default=''}</td>
+                <td class="audit-col-name"{if condition="$pr.order_rowspan gt 1"} rowspan="{$pr.order_rowspan}"{/if}>{$pr.CYJMC|default=''}</td>
                 {/if}
                 <td class="audit-process-name">{$pr.CGYMC|default=''}</td>
                 <td class="text-center">{$pr.CDW|default=''}</td>
                 <td class="text-center">{$pr.This_quantity|default=''}</td>
                 <td class="text-center">{$pr.ceilingPrice|default=''}</td>
                 <td class="text-center">{$pr.CDF|default=''}</td>
-                <td>{$pr.MBZ|default=''}</td>
+                <td class="audit-col-remark">{$pr.MBZ|default=''}</td>
             </tr>
             {/volist}
             </tbody>

+ 34 - 12
application/admin/view/procuremen/outward_detail.html

@@ -42,13 +42,20 @@
         min-width: 1320px;
         margin-bottom: 0;
     }
-    .outward-confirm-process-table,
+    .outward-confirm-process-table {
+        margin: 0;
+        font-size: 12px;
+        table-layout: fixed;
+        width: 100%;
+        min-width: 0;
+        max-width: 100%;
+    }
     .outward-confirm-company-table {
         margin: 0;
         font-size: 12px;
         table-layout: fixed;
-        width: 1680px;
-        min-width: 1680px;
+        width: 1580px;
+        min-width: 1580px;
         max-width: none;
     }
     .outward-confirm-process-table th:nth-child(3),
@@ -56,6 +63,19 @@
     .outward-confirm-company-table td.outward-process-name {
         white-space: nowrap;
     }
+    .outward-confirm-process-table th.outward-col-name,
+    .outward-confirm-process-table td.outward-col-name {
+        width: 26%;
+        white-space: normal;
+        word-break: break-word;
+    }
+    .outward-confirm-process-table th.outward-col-remark,
+    .outward-confirm-process-table td.outward-col-remark {
+        width: 120px;
+        max-width: 120px;
+        white-space: normal;
+        word-break: break-word;
+    }
     .outward-detail-table > thead > tr > th,
     .outward-detail-table > tbody > tr > td,
     .outward-confirm-process-table th,
@@ -71,8 +91,9 @@
     }
     .outward-confirm-company-table th.col-supplier-name,
     .outward-confirm-company-table td.col-supplier-name {
-        width: 200px;
-        min-width: 200px;
+        width: 150px;
+        min-width: 150px;
+        max-width: 150px;
         word-break: normal;
         overflow-wrap: break-word;
         white-space: normal;
@@ -80,8 +101,9 @@
     }
     .outward-confirm-company-table th.col-supplier-email,
     .outward-confirm-company-table td.col-supplier-email {
-        width: 160px;
-        min-width: 160px;
+        width: 120px;
+        min-width: 120px;
+        max-width: 120px;
         word-break: break-all;
         white-space: normal;
         line-height: 1.35;
@@ -380,18 +402,18 @@
                 <col style="width:72px"/>
                 <col style="width:72px"/>
                 <col style="width:80px"/>
-                <col/>
+                <col style="width:120px"/>
             </colgroup>
             <thead>
             <tr>
                 <th class="text-center">订单号</th>
-                <th>印件名称</th>
+                <th class="outward-col-name">印件名称</th>
                 <th>工序名称</th>
                 <th class="text-center">单位</th>
                 <th class="text-center">本次数量</th>
                 <th class="text-center">最高限价</th>
                 <th class="text-center">订法</th>
-                <th>备注</th>
+                <th class="outward-col-remark">备注</th>
             </tr>
             </thead>
             <tbody>
@@ -399,14 +421,14 @@
             <tr>
                 {if $pr.show_order_cells}
                 <td class="text-center"{if condition="$pr.order_rowspan gt 1"} rowspan="{$pr.order_rowspan}"{/if}>{$pr.CCYDH|default=''}</td>
-                <td{if condition="$pr.order_rowspan gt 1"} rowspan="{$pr.order_rowspan}"{/if}>{$pr.CYJMC|default=''}</td>
+                <td class="outward-col-name"{if condition="$pr.order_rowspan gt 1"} rowspan="{$pr.order_rowspan}"{/if}>{$pr.CYJMC|default=''}</td>
                 {/if}
                 <td class="outward-process-name">{$pr.CGYMC|default=''}</td>
                 <td class="text-center">{$pr.CDW|default=''}</td>
                 <td class="text-center">{$pr.This_quantity|default=''}</td>
                 <td class="text-center">{$pr.ceilingPrice|default=''}</td>
                 <td class="text-center">{$pr.CDF|default=''}</td>
-                <td>{$pr.MBZ|default=''}</td>
+                <td class="outward-col-remark">{$pr.MBZ|default=''}</td>
             </tr>
             {/volist}
             </tbody>

+ 92 - 23
application/admin/view/supplierservicescore/monthorders.html

@@ -1,8 +1,56 @@
 <style>
+    /* 弹层全屏:打通 FastAdmin #main/#content 高度链,表格区占满剩余高度 */
+    html, body.is-dialog {
+        height: 100% !important;
+        overflow: hidden !important;
+        margin: 0 !important;
+        background: #fff;
+    }
+    body.is-dialog {
+        display: flex !important;
+        flex-direction: column !important;
+    }
+    body.is-dialog #main,
+    body.is-dialog #main > .tab-content,
+    body.is-dialog #main #content {
+        height: 100% !important;
+        min-height: 0;
+        overflow: hidden;
+        display: flex;
+        flex-direction: column;
+    }
+    body.is-dialog #content > .row,
+    body.is-dialog #content > .row > [class*="col-"],
+    body.is-dialog .content {
+        height: 100% !important;
+        min-height: 0;
+        display: flex;
+        flex-direction: column;
+        box-sizing: border-box;
+        padding: 0 !important;
+        overflow: hidden;
+        margin: 0;
+    }
+    .ssc-month-orders-page {
+        flex: 1 1 auto;
+        min-height: 0;
+        height: 100%;
+        display: flex;
+        flex-direction: column;
+        overflow: hidden;
+        background: #fff;
+    }
     .ssc-month-orders-wrap {
+        flex: 1 1 auto;
+        min-height: 0;
         padding: 12px 16px 8px;
+        display: flex;
+        flex-direction: column;
+        box-sizing: border-box;
+        overflow: hidden;
     }
     .ssc-month-orders-head {
+        flex: 0 0 auto;
         margin-bottom: 12px;
         line-height: 1.6;
     }
@@ -15,14 +63,17 @@
         color: #333;
     }
     .ssc-month-orders-wrap .table-responsive {
+        flex: 1 1 0%;
+        min-height: 0;
         width: 100%;
-        overflow-x: auto;
+        overflow: auto;
         -webkit-overflow-scrolling: touch;
         border: 1px solid #eee;
+        background: #fff;
     }
     .ssc-month-orders-table {
         margin-bottom: 0;
-        min-width: 1280px;
+        min-width: 1680px;
     }
     .ssc-month-orders-table > thead > tr > th,
     .ssc-month-orders-table > tbody > tr > td {
@@ -36,6 +87,19 @@
         word-break: break-all;
         max-width: 220px;
     }
+    .ssc-month-orders-table .ssc-text-cell {
+        text-align: left;
+        white-space: normal;
+        word-break: break-word;
+        max-width: 200px;
+        min-width: 120px;
+    }
+    .ssc-month-orders-table > thead > tr > th {
+        position: sticky;
+        top: 0;
+        z-index: 2;
+        background: #f5f5f5;
+    }
     /* 操作列固定右侧,左右滚动时始终可见 */
     .ssc-month-orders-table > thead > tr > th.ssc-operate-col,
     .ssc-month-orders-table > tbody > tr > td.ssc-operate-col {
@@ -49,7 +113,8 @@
         border-left: 1px solid #e8e8e8 !important;
     }
     .ssc-month-orders-table > thead > tr > th.ssc-operate-col {
-        z-index: 4;
+        top: 0;
+        z-index: 5;
         background: #f5f5f5;
     }
     .ssc-month-orders-table.table-striped > tbody > tr:nth-of-type(odd) > td.ssc-operate-col {
@@ -59,7 +124,11 @@
         background: #f5f5f5;
     }
     .ssc-month-orders-empty {
-        padding: 48px 16px;
+        flex: 1 1 auto;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        padding: 24px 16px;
         text-align: center;
         color: #999;
     }
@@ -67,10 +136,17 @@
         color: #3c763d;
         font-weight: 600;
     }
+    .ssc-month-orders-page > .layer-footer {
+        flex: 0 0 auto;
+        padding: 10px 16px;
+        border-top: 1px solid #eee;
+        background: #fff;
+    }
 </style>
+<div class="ssc-month-orders-page">
 <div class="ssc-month-orders-wrap">
     <div class="ssc-month-orders-head">
-        <span class="ssc-meta">月份:{$ym|htmlentities}</span>
+        <span class="ssc-meta">日期:{$ym|htmlentities}</span>
         <span class="ssc-meta">供应商:<span class="ssc-company">{$company_name|htmlentities}</span></span>
         <span class="ssc-meta">共 {$orderCount} 单</span>
     </div>
@@ -90,10 +166,12 @@
                 <th>交货得分</th>
                 <th>状态</th>
                 <th>是否合格</th>
+                <th>不合格说明</th>
                 <th>交货情况</th>
+                <th>交货日期</th>
                 <th>客户是否投诉</th>
+                <th>投诉说明</th>
                 <th>订单是否中断</th>
-                <th>备注</th>
                 <th>当前进度</th>
                 <th>下发时间</th>
                 <th class="ssc-operate-col">操作</th>
@@ -120,35 +198,25 @@
                         {/if}
                     {/if}
                 </td>
-                <td>
-                    {if condition="$row.inbound_result eq '合格'"}
-                    <span class="label label-success">合格</span>
-                    {elseif condition="$row.inbound_result eq '不合格'"/}
-                    <span class="label label-danger">不合格</span>
-                    {/if}
-                </td>
-                <td>
-                    {if condition="$row.delivery_status eq '准时'"}
-                    <span class="label label-success">准时</span>
-                    {elseif condition="$row.delivery_status eq '滞后'"/}
-                    <span class="label label-warning">滞后</span>
-                    {/if}
-                </td>
+                <td>{$row.inbound_result|default=''}</td>
+                <td class="ssc-text-cell">{if $row.inbound_remark}{$row.inbound_remark|htmlentities}{/if}</td>
+                <td>{$row.delivery_status|default=''}</td>
+                <td>{$row.delivery_date|default=''}</td>
                 <td>
                     {if condition="$row.customer_complaint eq 1"}
-                    <span class="label label-warning"></span>
+                    是
                     {else/}
                     {/if}
                 </td>
+                <td class="ssc-text-cell">{if $row.complaint_remark}{$row.complaint_remark|htmlentities}{/if}</td>
                 <td>
                     {if condition="$row.order_interrupt eq 1"}
-                    <span class="label label-warning"></span>
+                    是
                     {else/}
                     {/if}
                 </td>
-                <td class="ssc-name-cell">{if $row.inbound_remark}{$row.inbound_remark|htmlentities}{/if}</td>
                 <td>
                     {if condition="$row.progress_text eq '已完结'"}
                     <span class="ssc-progress-done">已完结</span>
@@ -180,3 +248,4 @@
         <button type="button" style="margin-right:20px" class="btn btn-default btn-embossed btn-ssc-month-orders-close">关闭</button>
     </div>
 </div>
+</div>

+ 269 - 106
application/common/library/ProcuremenSupplierScore.php

@@ -644,8 +644,69 @@ class ProcuremenSupplierScore
         return abs($v) < 0.0000001;
     }
 
+    /**
+     * 工序完结年月(YYYY-MM)
+     * 优先:采购确认 / 直接完结操作时间;其次采购单 pick_time/createtime/dStamp;再无:入库评分时间
+     *
+     * @param int[]                                                    $scydgyIds
+     * @param array<int, array{createtime?:mixed,updatetime?:mixed}> $inboundTimeBySid
+     * @return array<int, string> scydgy_id => YYYY-MM
+     */
+    public static function loadCompletedYmMapByScydgyIds(array $scydgyIds, array $inboundTimeBySid = []): array
+    {
+        $ids = [];
+        foreach ($scydgyIds as $sid) {
+            $sid = (int)$sid;
+            if ($sid > 0) {
+                $ids[$sid] = true;
+            }
+        }
+        if ($ids === []) {
+            return [];
+        }
+        $sidList = array_keys($ids);
+        $completeTsMap = ProcuremenTime::loadOperLogTimestampMap($sidList, ['purchase_confirm', 'mark_complete']);
+        $poBySid = [];
+        try {
+            $poRows = Db::table('purchase_order')
+                ->where('scydgy_id', 'in', $sidList)
+                ->field('scydgy_id,pick_time,createtime,dStamp')
+                ->select();
+        } catch (\Throwable $e) {
+            $poRows = [];
+        }
+        if (is_array($poRows)) {
+            foreach ($poRows as $po) {
+                if (!is_array($po)) {
+                    continue;
+                }
+                $sid = (int)($po['scydgy_id'] ?? 0);
+                if ($sid > 0) {
+                    $poBySid[$sid] = $po;
+                }
+            }
+        }
+        $out = [];
+        foreach ($sidList as $sid) {
+            $row = $poBySid[$sid] ?? ['scydgy_id' => $sid];
+            $done = ProcuremenTime::resolveCompletedDone($row, $completeTsMap);
+            $ts = (int)($done['ts'] ?? 0);
+            if ($ts <= 946684800 && isset($inboundTimeBySid[$sid])) {
+                $t1 = ProcuremenTime::parseToTimestamp($inboundTimeBySid[$sid]['updatetime'] ?? null);
+                $t2 = ProcuremenTime::parseToTimestamp($inboundTimeBySid[$sid]['createtime'] ?? null);
+                $ts = max($t1, $t2);
+            }
+            if ($ts > 946684800) {
+                $out[$sid] = date('Y-m', $ts);
+            }
+        }
+
+        return $out;
+    }
+
     /**
      * 某供应商某月入库评分订单聚合(按订单号去重)
+     * 月份按工序「完结时间」归属,不用质量评分创建/更新时间
      *
      * @return array<string, array{result:string,delivery_status:string,customer_complaint:int,order_interrupt:int}>
      */
@@ -661,70 +722,102 @@ class ProcuremenSupplierScore
         try {
             $rows = Db::table('purchase_order_inbound_score')
                 ->where('company_name', $companyName)
-                ->where(function ($q) use ($ym) {
-                    $q->where('updatetime', 'like', $ym . '%')
-                        ->whereOr('createtime', 'like', $ym . '%');
-                })
-                ->field('ccydh,result,delivery_status,customer_complaint,order_interrupt')
+                ->field('scydgy_id,ccydh,result,delivery_status,customer_complaint,order_interrupt,createtime,updatetime')
                 ->select();
         } catch (\Throwable $e) {
             try {
                 $rows = Db::table('purchase_order_inbound_score')
                     ->where('company_name', $companyName)
-                    ->where(function ($q) use ($ym) {
-                        $q->where('updatetime', 'like', $ym . '%')
-                            ->whereOr('createtime', 'like', $ym . '%');
-                    })
-                    ->field('ccydh,result')
+                    ->field('scydgy_id,ccydh,result,createtime,updatetime')
                     ->select();
             } catch (\Throwable $e2) {
                 $rows = [];
                 Log::write('loadInboundOrderAgg table: ' . $e2->getMessage(), 'error');
             }
         }
-        if (is_array($rows)) {
-            foreach ($rows as $r) {
-                if (!is_array($r)) {
-                    continue;
-                }
-                $ccydh = trim((string)($r['ccydh'] ?? ''));
-                $res = trim((string)($r['result'] ?? ''));
-                if ($ccydh === '' || ($res !== '合格' && $res !== '不合格')) {
-                    continue;
-                }
-                $ds = trim((string)($r['delivery_status'] ?? ''));
-                if ($ds !== '准时' && $ds !== '滞后') {
-                    $ds = '';
-                }
-                $complaint = (int)($r['customer_complaint'] ?? 0) === 1 ? 1 : 0;
-                $interrupt = (int)($r['order_interrupt'] ?? 0) === 1 ? 1 : 0;
-                if (!isset($byOrder[$ccydh])) {
-                    $byOrder[$ccydh] = [
-                        'result'             => $res,
-                        'delivery_status'    => $ds,
-                        'customer_complaint' => $complaint,
-                        'order_interrupt'    => $interrupt,
-                    ];
-                    continue;
-                }
-                if ($res === '不合格') {
-                    $byOrder[$ccydh]['result'] = '不合格';
-                }
-                if ($byOrder[$ccydh]['delivery_status'] === '' && $ds !== '') {
-                    $byOrder[$ccydh]['delivery_status'] = $ds;
-                }
-                if ($complaint === 1) {
-                    $byOrder[$ccydh]['customer_complaint'] = 1;
-                }
-                if ($interrupt === 1) {
-                    $byOrder[$ccydh]['order_interrupt'] = 1;
-                }
+        if (!is_array($rows) || $rows === []) {
+            $legacy = self::loadInboundOrderResultsFromOperLogs($companyName, $ym);
+            foreach ($legacy as $ccydh => $res) {
+                $byOrder[$ccydh] = [
+                    'result'             => $res,
+                    'delivery_status'    => '',
+                    'customer_complaint' => 0,
+                    'order_interrupt'    => 0,
+                ];
+            }
+
+            return $byOrder;
+        }
+        $sidSet = [];
+        $inboundTimeBySid = [];
+        foreach ($rows as $r) {
+            if (!is_array($r)) {
+                continue;
+            }
+            $sid = (int)($r['scydgy_id'] ?? 0);
+            if ($sid <= 0) {
+                continue;
+            }
+            $sidSet[$sid] = true;
+            $inboundTimeBySid[$sid] = [
+                'createtime' => $r['createtime'] ?? null,
+                'updatetime' => $r['updatetime'] ?? null,
+            ];
+        }
+        $completedYmMap = self::loadCompletedYmMapByScydgyIds(array_keys($sidSet), $inboundTimeBySid);
+        foreach ($rows as $r) {
+            if (!is_array($r)) {
+                continue;
+            }
+            $sid = (int)($r['scydgy_id'] ?? 0);
+            $rowYm = $sid > 0 ? (string)($completedYmMap[$sid] ?? '') : '';
+            if ($rowYm === '' && $sid <= 0) {
+                // 无工序 id:回退评分时间
+                $t1 = ProcuremenTime::parseToTimestamp($r['updatetime'] ?? null);
+                $t2 = ProcuremenTime::parseToTimestamp($r['createtime'] ?? null);
+                $ts = max($t1, $t2);
+                $rowYm = $ts > 946684800 ? date('Y-m', $ts) : '';
+            }
+            if ($rowYm !== $ym) {
+                continue;
+            }
+            $ccydh = trim((string)($r['ccydh'] ?? ''));
+            $res = trim((string)($r['result'] ?? ''));
+            if ($ccydh === '' || ($res !== '合格' && $res !== '不合格')) {
+                continue;
+            }
+            $ds = trim((string)($r['delivery_status'] ?? ''));
+            if ($ds !== '准时' && $ds !== '滞后') {
+                $ds = '';
+            }
+            $complaint = (int)($r['customer_complaint'] ?? 0) === 1 ? 1 : 0;
+            $interrupt = (int)($r['order_interrupt'] ?? 0) === 1 ? 1 : 0;
+            if (!isset($byOrder[$ccydh])) {
+                $byOrder[$ccydh] = [
+                    'result'             => $res,
+                    'delivery_status'    => $ds,
+                    'customer_complaint' => $complaint,
+                    'order_interrupt'    => $interrupt,
+                ];
+                continue;
+            }
+            if ($res === '不合格') {
+                $byOrder[$ccydh]['result'] = '不合格';
+            }
+            if ($byOrder[$ccydh]['delivery_status'] === '' && $ds !== '') {
+                $byOrder[$ccydh]['delivery_status'] = $ds;
+            }
+            if ($complaint === 1) {
+                $byOrder[$ccydh]['customer_complaint'] = 1;
+            }
+            if ($interrupt === 1) {
+                $byOrder[$ccydh]['order_interrupt'] = 1;
             }
         }
         if ($byOrder !== []) {
             return $byOrder;
         }
-        // 回退:操作日志仅能还原合格/不合格
+        // 回退:操作日志仅能还原合格/不合格(同样按完结月过滤)
         $legacy = self::loadInboundOrderResultsFromOperLogs($companyName, $ym);
         foreach ($legacy as $ccydh => $res) {
             $byOrder[$ccydh] = [
@@ -754,7 +847,7 @@ class ProcuremenSupplierScore
     }
 
     /**
-     * 操作日志回退:订单号 => 合格|不合格
+     * 操作日志回退:订单号 => 合格|不合格(按工序完结月过滤)
      *
      * @return array<string, string>
      */
@@ -766,7 +859,6 @@ class ProcuremenSupplierScore
                 ->where(function ($q) {
                     $q->where('action', '入库评分')->whereOr('action', 'inbound_score');
                 })
-                ->where('createtime', 'like', $ym . '%')
                 ->field('scydgy_id,content,createtime')
                 ->order('id', 'asc')
                 ->select();
@@ -779,6 +871,7 @@ class ProcuremenSupplierScore
             return [];
         }
         $sidResult = [];
+        $inboundTimeBySid = [];
         foreach ($logs as $lg) {
             if (!is_array($lg)) {
                 continue;
@@ -792,6 +885,10 @@ class ProcuremenSupplierScore
                 continue;
             }
             $sidResult[$sid] = $m[1];
+            $inboundTimeBySid[$sid] = [
+                'createtime' => $lg['createtime'] ?? null,
+                'updatetime' => $lg['createtime'] ?? null,
+            ];
         }
         if ($sidResult === []) {
             return [];
@@ -808,6 +905,7 @@ class ProcuremenSupplierScore
         if (!is_array($poRows)) {
             return [];
         }
+        $completedYmMap = self::loadCompletedYmMapByScydgyIds(array_keys($sidResult), $inboundTimeBySid);
         foreach ($poRows as $po) {
             if (!is_array($po)) {
                 continue;
@@ -818,6 +916,9 @@ class ProcuremenSupplierScore
             if ($sid <= 0 || $ccydh === '' || ($res !== '合格' && $res !== '不合格')) {
                 continue;
             }
+            if ((string)($completedYmMap[$sid] ?? '') !== $ym) {
+                continue;
+            }
             if (!isset($byOrder[$ccydh]) || $res === '不合格') {
                 $byOrder[$ccydh] = $res;
             }
@@ -998,8 +1099,9 @@ class ProcuremenSupplierScore
 
     /**
      * 增值服务(满分 10):未报价订单数(按订单号去重)≤3 →10;>3 →0
+     * 须当月已有完结/入库评分订单才计分(与本月订单明细同源);仅有询价无完结单 → null
      *
-     * @return float|null 当月无询价记录时返回 null
+     * @return float|null 无本月完结订单或无询价记录时返回 null
      */
     public static function calcValueAddedScoreFromUnquoted(string $companyName, string $ym): ?float
     {
@@ -1008,6 +1110,11 @@ class ProcuremenSupplierScore
         if ($companyName === '' || $ym === '') {
             return null;
         }
+        // 无本月入库完结订单则不计增值(避免「明细 0 单却有 10 分」)
+        $inboundOrders = self::loadInboundOrderAggByCompanyYm($companyName, $ym);
+        if ($inboundOrders === []) {
+            return null;
+        }
         $stats = self::loadUnquotedOrderStatsByCompanyYm($companyName, $ym);
         if ($stats === null) {
             return null;
@@ -1092,7 +1199,8 @@ class ProcuremenSupplierScore
     }
 
     /**
-     * 收集某月需重算的供应商名称(入库评分 + 当月询价明细)
+     * 收集某月需重算的供应商名称
+     * (按完结月有入库评分的供应商 + 当月询价明细 + 月度表已有行,便于清掉错月脏数据)
      *
      * @return string[]
      */
@@ -1107,77 +1215,105 @@ class ProcuremenSupplierScore
             return [$onlyCompany];
         }
         $companies = [];
+        // 1) 当月完结(采购确认/直接完结)的工序 → 供应商
+        $ymEsc = str_replace(["\\", "'", '%', '_'], ["\\\\", "''", '\\%', '\\_'], $ym);
         try {
-            $rows = Db::table('purchase_order_inbound_score')
-                ->where(function ($q) use ($ym) {
-                    $q->where('updatetime', 'like', $ym . '%')
-                        ->whereOr('createtime', 'like', $ym . '%');
-                })
-                ->field('company_name')
-                ->group('company_name')
+            $completeActs = ProcuremenOperLog::expandActionQueryValues(['purchase_confirm', 'mark_complete']);
+            $clog = Db::table('purchase_order_oper_log')
+                ->where(ProcuremenOperLog::COL_ACTION, 'in', $completeActs)
+                ->where(ProcuremenOperLog::COL_TIME, 'like', $ym . '%')
+                ->field(ProcuremenOperLog::COL_SCYDGY_ID)
                 ->select();
         } catch (\Throwable $e) {
-            $rows = [];
-            Log::write('collectMonthlyReviewCompanyNames inbound: ' . $e->getMessage(), 'error');
+            $clog = [];
         }
-        if (is_array($rows)) {
-            foreach ($rows as $r) {
-                if (!is_array($r)) {
+        $completeSids = [];
+        if (is_array($clog)) {
+            foreach ($clog as $lg) {
+                if (!is_array($lg)) {
                     continue;
                 }
-                $cn = trim((string)($r['company_name'] ?? ''));
-                if ($cn !== '') {
-                    $companies[$cn] = true;
+                $sid = (int)($lg[ProcuremenOperLog::COL_SCYDGY_ID] ?? $lg['scydgy_id'] ?? 0);
+                if ($sid > 0) {
+                    $completeSids[$sid] = true;
                 }
             }
         }
+        // 2) 全部有入库评分的供应商:再按完结月精确过滤
         try {
-            $logs = Db::table('purchase_order_oper_log')
-                ->where(function ($q) {
-                    $q->where('action', '入库评分')->whereOr('action', 'inbound_score');
-                })
-                ->where('createtime', 'like', $ym . '%')
-                ->field('scydgy_id')
+            $scoreRows = Db::table('purchase_order_inbound_score')
+                ->field('scydgy_id,company_name,createtime,updatetime')
                 ->select();
         } catch (\Throwable $e) {
-            $logs = [];
+            $scoreRows = [];
+            Log::write('collectMonthlyReviewCompanyNames inbound: ' . $e->getMessage(), 'error');
         }
-        $sids = [];
-        if (is_array($logs)) {
-            foreach ($logs as $lg) {
-                if (!is_array($lg)) {
+        $sidSet = $completeSids;
+        $inboundTimeBySid = [];
+        $companyBySid = [];
+        if (is_array($scoreRows)) {
+            foreach ($scoreRows as $r) {
+                if (!is_array($r)) {
                     continue;
                 }
-                $sid = (int)($lg['scydgy_id'] ?? 0);
+                $sid = (int)($r['scydgy_id'] ?? 0);
+                $cn = trim((string)($r['company_name'] ?? ''));
                 if ($sid > 0) {
-                    $sids[$sid] = true;
+                    $sidSet[$sid] = true;
+                    $inboundTimeBySid[$sid] = [
+                        'createtime' => $r['createtime'] ?? null,
+                        'updatetime' => $r['updatetime'] ?? null,
+                    ];
+                    if ($cn !== '') {
+                        $companyBySid[$sid] = $cn;
+                    }
+                } elseif ($cn !== '') {
+                    // 无工序 id:按评分时间归月
+                    $t1 = ProcuremenTime::parseToTimestamp($r['updatetime'] ?? null);
+                    $t2 = ProcuremenTime::parseToTimestamp($r['createtime'] ?? null);
+                    $ts = max($t1, $t2);
+                    if ($ts > 946684800 && date('Y-m', $ts) === $ym) {
+                        $companies[$cn] = true;
+                    }
                 }
             }
         }
-        if ($sids !== []) {
-            try {
-                $poRows = Db::table('purchase_order')
-                    ->where('scydgy_id', 'in', array_keys($sids))
-                    ->field('pick_company_name')
-                    ->group('pick_company_name')
-                    ->select();
-            } catch (\Throwable $e) {
-                $poRows = [];
+        if ($sidSet !== []) {
+            $completedYmMap = self::loadCompletedYmMapByScydgyIds(array_keys($sidSet), $inboundTimeBySid);
+            $needPoNames = [];
+            foreach ($completedYmMap as $sid => $rowYm) {
+                if ($rowYm !== $ym) {
+                    continue;
+                }
+                if (isset($companyBySid[$sid]) && $companyBySid[$sid] !== '') {
+                    $companies[$companyBySid[$sid]] = true;
+                } else {
+                    $needPoNames[(int)$sid] = true;
+                }
             }
-            if (is_array($poRows)) {
-                foreach ($poRows as $po) {
-                    if (!is_array($po)) {
-                        continue;
-                    }
-                    $cn = trim((string)($po['pick_company_name'] ?? ''));
-                    if ($cn !== '') {
-                        $companies[$cn] = true;
+            if ($needPoNames !== []) {
+                try {
+                    $poRows = Db::table('purchase_order')
+                        ->where('scydgy_id', 'in', array_keys($needPoNames))
+                        ->field('scydgy_id,pick_company_name')
+                        ->select();
+                } catch (\Throwable $e) {
+                    $poRows = [];
+                }
+                if (is_array($poRows)) {
+                    foreach ($poRows as $po) {
+                        if (!is_array($po)) {
+                            continue;
+                        }
+                        $cn = trim((string)($po['pick_company_name'] ?? ''));
+                        if ($cn !== '') {
+                            $companies[$cn] = true;
+                        }
                     }
                 }
             }
         }
-        // 并入当月有询价明细的供应商(增值服务未报价统计)
-        $ymEsc = str_replace(["\\", "'", '%', '_'], ["\\\\", "''", '\\%', '\\_'], $ym);
+        // 3) 并入当月有询价明细的供应商(增值服务未报价统计)
         try {
             $detailRows = Db::table('purchase_order_detail')
                 ->alias('d')
@@ -1211,6 +1347,26 @@ class ProcuremenSupplierScore
                 }
             }
         }
+        // 4) 月度表已有行:重算时可清掉错月残留
+        try {
+            $finalRows = Db::table(self::TABLE_FINAL)
+                ->where('ym', $ym)
+                ->field('company_name')
+                ->select();
+        } catch (\Throwable $e) {
+            $finalRows = [];
+        }
+        if (is_array($finalRows)) {
+            foreach ($finalRows as $fr) {
+                if (!is_array($fr)) {
+                    continue;
+                }
+                $cn = trim((string)($fr['company_name'] ?? ''));
+                if ($cn !== '') {
+                    $companies[$cn] = true;
+                }
+            }
+        }
 
         return array_keys($companies);
     }
@@ -1251,7 +1407,11 @@ class ProcuremenSupplierScore
                 $quality = self::calcQualityScoreFromInbound($cn, $ym);
                 $price = self::calcPriceScoreFromCeiling($cn, $ym);
                 $delivery = self::calcDeliveryScoreFromInbound($cn, $ym);
-                $valueAdded = self::calcValueAddedScoreFromUnquoted($cn, $ym);
+                // 与「本月订单明细」一致:当月无完结/入库评分订单时,增值也不单独给分
+                $valueAdded = null;
+                if ($quality !== null || $price !== null || $delivery !== null) {
+                    $valueAdded = self::calcValueAddedScoreFromUnquoted($cn, $ym);
+                }
                 $final = self::sumFinalScore($quality, $price, $delivery, $valueAdded);
                 $grade = $final !== null ? self::gradeFromFinalScore($final) : '';
                 $scoreCompat = $final !== null ? $final : 0.0;
@@ -1423,21 +1583,24 @@ class ProcuremenSupplierScore
     }
 
     /**
-     * 读取某月评审记录(仅读月度表;先按当月开标/询价供应商补齐名称
+     * 读取某月评审记录(仅读月度表)
      *
+     * @param bool $resync true=先补齐供应商并按规则重算;false=只读库(导出用,避免卡顿)
      * @return array<string, array{score:float,quality_score:?float,price_score:?float,delivery_score:?float,final_score:?float,final_saved:int,score_grade:string,score_date:string}>
      */
-    public static function loadMonthlyMapByYm(string $ym): array
+    public static function loadMonthlyMapByYm(string $ym, bool $resync = true): array
     {
         $ym = trim($ym);
         if (!preg_match('/^\d{4}-\d{2}$/', $ym)) {
             return [];
         }
         self::ensureFinalScoreTable();
-        // 本月询价(开标后已写入订单评分表)的供应商:名称不存在则加空行,已存在不动
-        self::ensureMonthlySupplierNamesFromOrderYm($ym);
-        // 按月度规则重算质量/价格/交货/增值(满分105;qp_manual=1 跳过)
-        self::syncMonthlyReviewScoresFromRules($ym);
+        if ($resync) {
+            // 本月询价(开标后已写入订单评分表)的供应商:名称不存在则加空行,已存在不动
+            self::ensureMonthlySupplierNamesFromOrderYm($ym);
+            // 按月度规则重算质量/价格/交货/增值(满分105;qp_manual=1 跳过)
+            self::syncMonthlyReviewScoresFromRules($ym);
+        }
         try {
             $rows = Db::table(self::TABLE_FINAL)
                 ->where('ym', $ym)

+ 10 - 2
application/extra/mproc.php

@@ -2,10 +2,12 @@
 
 /**
  * 手机端协助明细(purchase_order_detail)登录与展示
- * 手机验证码:仅 customer 表登记手机号为普通外协(按 customer 公司名筛明细
+ * 手机验证码:先 customer(供应商),未命中再 admin.mobile(操作人员
  * 账号密码:先 customer,未命中再 admin(fa_admin)
  *   - 供应商(customer)登录后 → 报价列表(可填单价/交期)
- *   - 管理员(admin)登录后 → 外协入库评分(合格/不合格)
+ *   - 质检中心(auth_group)→ 质量评分(合格/投诉/中断)
+ *   - 生产部/生产员(auth_group)→ 交货情况(准时/滞后)
+ *   - 其它后台账号 → 提示「账号或密码错误」(不可进手机端)
  */
 return [
     // 邮件根地址(正式环境);本地未配或校验不通过时自动用当前访问域名
@@ -29,6 +31,12 @@ return [
     // 新增询价「通知业务员」:仅该角色组(默认「业务员」id=12)下的管理员
     'rfq_notify_salesman_group_id' => 12,
 
+    // 手机端质量评分:所属角色组(默认「质检中心」)
+    'mobile_quality_group_id'   => 16,
+    // 手机端交货情况:所属角色组(默认「生产员」,展示名「生产部」)
+    'mobile_delivery_group_id'  => 15,
+    'mobile_delivery_dept_name' => '生产部',
+
     // 当前 login.html 对应哪个系统(换登录页同步改这里)
     // collab = 供应链协同;
     // cockpit = 生产经营驾驶舱

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 832 - 159
application/index/controller/Index.php


+ 672 - 0
application/index/view/index/deliveryscore.html

@@ -0,0 +1,672 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
+    <title>交货情况</title>
+    <meta property="og:title" content="交货情况">
+    <meta property="og:image" content="{:htmlentities(default_admin_avatar_url())}">
+    <link rel="shortcut icon" href="{:htmlentities(default_admin_avatar_url())}" type="image/png">
+    <link rel="icon" href="{:htmlentities(default_admin_avatar_url())}" type="image/png">
+    <link rel="apple-touch-icon" href="{:htmlentities(default_admin_avatar_url())}">
+    <style>
+        * { box-sizing: border-box; }
+        html, body { margin: 0; height: 100%; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; background: #f5f5f5; color: #333; }
+        :root {
+            --bar-h: 52px;
+            --tabbar-h: 52px;
+            --side-w: 72px;
+            --safe-bottom: env(safe-area-inset-bottom, 0px);
+        }
+        body.layout-orders { overflow: hidden; }
+        body.layout-me { overflow: hidden; }
+        .bar {
+            position: fixed; top: 0; left: 0; right: 0; z-index: 50;
+            height: var(--bar-h); padding: 0 14px;
+            background: #3c8dbc; color: #fff;
+            display: flex; align-items: center; justify-content: space-between;
+        }
+        .bar h1 { font-size: 1.05rem; margin: 0; font-weight: 600; }
+        .bar a { color: #fff; text-decoration: none; font-size: 13px; padding: 5px 10px; border: 1px solid rgba(255,255,255,.6); border-radius: 6px; }
+        body.layout-orders .bar-logout { display: none !important; }
+        #pane-orders {
+            position: fixed; left: 0; right: 0; z-index: 10;
+            top: var(--bar-h); bottom: calc(var(--tabbar-h) + var(--safe-bottom));
+            display: flex; flex-direction: column; overflow: hidden; background: #f5f5f5;
+        }
+        #pane-me {
+            position: fixed; left: 0; right: 0; z-index: 10;
+            top: var(--bar-h); bottom: calc(var(--tabbar-h) + var(--safe-bottom));
+            overflow-y: auto; -webkit-overflow-scrolling: touch; background: #f5f5f5;
+            display: none;
+        }
+        .search-wrap {
+            flex-shrink: 0; padding: 10px 12px; background: #e8f4fc;
+            border-bottom: 1px solid #d2e7f4; width: 100%;
+        }
+        .search-form { display: flex; gap: 8px; align-items: center; max-width: 640px; margin: 0 auto; }
+        .search-form input[type="search"],
+        .search-form input[type="text"] {
+            flex: 1; min-width: 0; padding: 10px 12px; border: 1px solid #bcd8ea;
+            border-radius: 8px; font-size: 15px; background: #fff;
+        }
+        .search-form input:focus { outline: none; border-color: #3c8dbc; }
+        .search-form button[type="submit"] {
+            flex-shrink: 0; padding: 10px 16px; border: none; border-radius: 8px;
+            background: #3c8dbc; color: #fff; font-size: 14px; font-weight: 600;
+        }
+        .body-wrap { flex: 1 1 0%; min-height: 0; display: flex; overflow: hidden; }
+        .side {
+            width: var(--side-w); flex-shrink: 0; background: #fff;
+            border-right: 1px solid #e5e5e5; padding-top: 8px; overflow-y: auto;
+        }
+        .side .cat {
+            display: block; width: 100%; padding: 14px 6px; border: none;
+            background: transparent; color: #555; font-size: 13px;
+            text-align: center; line-height: 1.35; font-family: inherit;
+            -webkit-tap-highlight-color: transparent;
+        }
+        .side .cat.active {
+            background: #f0f7fb; color: #3c8dbc; font-weight: 600;
+            box-shadow: inset 3px 0 0 #3c8dbc;
+        }
+        .card .score-summary {
+            margin-top: 8px; font-size: 12px; color: #666; line-height: 1.5;
+            display: flex; flex-wrap: wrap; align-items: center; gap: 6px 10px;
+        }
+        .card .score-summary .res {
+            font-weight: 700; color: #27ae60;
+        }
+        .card .score-summary .res.is-fail { color: #e74c3c; }
+        .card .score-extra { margin-top: 6px; font-size: 12px; color: #666; line-height: 1.5; }
+        .card .score-time { margin-top: 6px; font-size: 12px; color: #888; }
+        .main {
+            flex: 1; min-width: 0; overflow-y: auto; -webkit-overflow-scrolling: touch;
+            padding: 10px; background: #f5f5f5;
+        }
+        .card {
+            background: #fff; border-radius: 10px; padding: 12px;
+            margin-bottom: 10px; box-shadow: 0 1px 4px rgba(0,0,0,.06);
+        }
+        .card .ord {
+            font-size: 15px; font-weight: 700; color: #222; margin-bottom: 8px;
+            line-height: 1.45; word-break: break-all;
+        }
+        .card .meta { font-size: 13px; color: #555; line-height: 1.55; }
+        .card .meta b { color: #333; font-weight: 600; }
+        .actions { display: flex; gap: 8px; margin-top: 10px; justify-content: flex-end; }
+        .actions button {
+            min-width: 72px; padding: 6px 14px; border: none; border-radius: 6px;
+            font-size: 13px; font-weight: 600; color: #fff; font-family: inherit;
+            -webkit-tap-highlight-color: transparent;
+        }
+        .btn-score { background: #3c8dbc; }
+        .btn-score:disabled { opacity: .55; }
+        .me-panel { padding: 16px 14px 24px; max-width: 480px; margin: 0 auto; }
+        .me-card { background: #fff; border-radius: 12px; padding: 18px 16px; box-shadow: 0 1px 6px rgba(0,0,0,.06); }
+        .me-row { font-size: 14px; line-height: 1.7; padding: 8px 0; border-bottom: 1px solid #f0f0f0; }
+        .me-row:last-child { border-bottom: none; }
+        .me-row span { display: inline-block; min-width: 4.5em; color: #888; }
+        .btn-me-logout {
+            display: block; width: 100%; margin-top: 16px; padding: 12px;
+            border: 1px solid #e74c3c; border-radius: 8px; background: #fff;
+            color: #e74c3c; font-size: 15px; font-weight: 600; text-align: center;
+            text-decoration: none; font-family: inherit;
+        }
+        .tabbar {
+            position: fixed; left: 0; right: 0; bottom: 0; z-index: 40;
+            display: flex; background: #fff; border-top: 1px solid #e5e5e5;
+            box-shadow: 0 -2px 10px rgba(0,0,0,.05);
+            padding-bottom: var(--safe-bottom);
+        }
+        .tabbar .tabbar-btn {
+            flex: 1; text-align: center; padding: 10px 4px 12px; color: #666;
+            font-size: 14px; line-height: 1.25; border: none; border-top: 3px solid transparent;
+            margin-top: -1px; background: #fff; cursor: pointer; font-family: inherit;
+            -webkit-tap-highlight-color: transparent;
+        }
+        .tabbar .tabbar-btn.active { color: #3c8dbc; font-weight: 600; border-top-color: #3c8dbc; background: #f8fbfd; }
+        .confirm-mask {
+            display: none; position: fixed; inset: 0; z-index: 2000;
+            background: rgba(0,0,0,.45); align-items: center; justify-content: center;
+            padding: 24px 28px;
+            padding-bottom: calc(24px + var(--safe-bottom));
+        }
+        .confirm-mask.show { display: flex; }
+        .confirm-box {
+            width: 100%; max-width: 340px; background: #fff;
+            border-radius: 12px;
+            box-shadow: 0 8px 28px rgba(0,0,0,.18);
+            max-height: min(90vh, 720px);
+            display: flex; flex-direction: column;
+            overflow: hidden;
+        }
+        .confirm-box-body {
+            flex: 1 1 auto;
+            min-height: 0;
+            overflow-y: auto;
+            -webkit-overflow-scrolling: touch;
+            padding: 18px 16px 12px;
+        }
+        .confirm-box .confirm-ord {
+            font-size: 15px; font-weight: 700; color: #222; line-height: 1.45;
+            word-break: break-all; margin: 0 0 8px; text-align: left;
+        }
+        .confirm-box .confirm-meta {
+            font-size: 13px; color: #555; line-height: 1.55; margin: 0 0 12px; text-align: left;
+        }
+        .confirm-box .confirm-meta b { color: #333; font-weight: 600; }
+        .score-field {
+            margin: 0 0 12px;
+            display: flex;
+            align-items: center;
+            gap: 10px;
+        }
+        .score-field .label {
+            flex: 0 0 auto;
+            min-width: 7.2em;
+            font-size: 13px; font-weight: 600; color: #333;
+            margin: 0; line-height: 1.35;
+        }
+        .score-field .label .req { color: #e74c3c; margin-left: 2px; }
+        .score-opts {
+            flex: 1 1 auto;
+            display: flex; gap: 8px;
+            min-width: 0;
+        }
+        .score-opt {
+            flex: 1; position: relative;
+        }
+        .score-opt input {
+            position: absolute; opacity: 0; pointer-events: none;
+        }
+        .score-opt span {
+            display: block; text-align: center; padding: 8px 6px; border-radius: 8px;
+            border: 1px solid #d9d9d9; background: #fafafa; font-size: 13px; font-weight: 600;
+            color: #555;
+        }
+        .score-opt input:checked + span {
+            border-color: #3c8dbc; background: #eaf5fb; color: #3c8dbc;
+        }
+        .score-opt.pass input:checked + span { border-color: #27ae60; background: #eafaf1; color: #27ae60; }
+        .score-opt.fail input:checked + span { border-color: #e74c3c; background: #fdecea; color: #e74c3c; }
+        .score-field-remark {
+            flex-direction: column;
+            align-items: stretch;
+            gap: 8px;
+        }
+        .score-field-remark .label {
+            min-width: 0;
+        }
+        .score-remark {
+            width: 100%; min-height: 72px; padding: 10px 12px; border: 1px solid #d9d9d9;
+            border-radius: 8px; font-size: 14px; line-height: 1.5; resize: vertical;
+            font-family: inherit; background: #fff; color: #333;
+        }
+        .score-remark:focus { outline: none; border-color: #3c8dbc; }
+        .score-date-wrap {
+            flex: 1 1 auto; min-width: 0; position: relative;
+            border: 1px solid #d9d9d9; border-radius: 8px; background: #fff;
+            cursor: pointer;
+        }
+        .score-date-wrap:focus-within { border-color: #3c8dbc; }
+        .score-date {
+            display: block; width: 100%; min-height: 40px; padding: 8px 10px;
+            border: none; border-radius: 8px; font-size: 14px; line-height: 1.4;
+            font-family: inherit; background: transparent; color: #333;
+            -webkit-appearance: none; appearance: none; cursor: pointer;
+        }
+        .score-date:focus { outline: none; }
+        .score-date::-webkit-calendar-picker-indicator {
+            position: absolute; left: 0; top: 0; width: 100%; height: 100%;
+            margin: 0; padding: 0; opacity: 0; cursor: pointer;
+        }
+        .confirm-box .confirm-acts {
+            flex: 0 0 auto;
+            display: flex; gap: 10px;
+            padding: 12px 16px;
+            border-top: 1px solid #eee;
+            background: #fff;
+        }
+        .confirm-box .confirm-acts button {
+            flex: 1; padding: 12px 8px; border: none; border-radius: 8px;
+            font-size: 15px; font-weight: 600; font-family: inherit;
+        }
+        .confirm-cancel { background: #f0f0f0; color: #555; }
+        .confirm-ok { background: #3c8dbc; color: #fff; }
+        .confirm-ok:disabled { opacity: .55; }
+        .empty { text-align: center; color: #999; padding: 48px 16px; font-size: 14px; }
+        .toast {
+            position: fixed; left: 50%; bottom: 90px; transform: translateX(-50%);
+            max-width: 86vw; padding: 10px 16px; border-radius: 8px;
+            background: rgba(0,0,0,.78); color: #fff; font-size: 14px;
+            z-index: 1000; opacity: 0; pointer-events: none; transition: opacity .2s;
+        }
+        .toast.show { opacity: 1; }
+        .toast.ok { background: #27ae60; }
+        .toast.err { background: #c0392b; }
+    </style>
+</head>
+<body class="layout-orders">
+<div class="bar">
+    <h1 id="barTitle">交货情况</h1>
+    <a href="{:url('index/index/logout')}" class="bar-logout" id="barLogout">退出</a>
+</div>
+
+<div id="pane-orders">
+    <div class="search-wrap">
+        <form class="search-form" id="searchForm" action="javascript:;" onsubmit="return false;">
+            <input type="search" id="qInput" name="q" placeholder="请输入搜索内容" value="{$mprocSearchQ|default=''|htmlentities}" maxlength="120" enterkeyhint="search" autocomplete="off">
+            <button type="submit">搜索</button>
+        </form>
+    </div>
+    <div class="body-wrap">
+        <div class="side" id="listCats">
+            <button type="button" class="cat active" data-list-tab="pending">待确认</button>
+            <button type="button" class="cat" data-list-tab="scored">已确认</button>
+        </div>
+        <div class="main" id="listMain">
+            {if $rows}
+            {volist name="rows" id="row"}
+            <div class="card" data-ccydh="{$row.CCYDH|htmlentities}">
+                <div class="ord">{$row.CCYDH|default=''|htmlentities}{if $row.CYJMC} {$row.CYJMC|htmlentities}{/if}</div>
+                <div class="meta">
+                    <div><b>供应商:</b>{$row.pick_company_name|default='—'|htmlentities}</div>
+                    <div><b>工序:</b>{$row.CGYMC|default='—'|htmlentities}</div>
+                </div>
+                <div class="actions">
+                    <button type="button" class="btn-score">确认交货</button>
+                </div>
+            </div>
+            {/volist}
+            {else/}
+            <div class="empty" id="emptyTip">暂无待确认订单</div>
+            {/if}
+        </div>
+    </div>
+</div>
+
+<div id="pane-me" class="me-panel">
+    <div class="me-card">
+        <div class="me-row"><span>账号</span>{$mprocProfile.account|default=''|htmlentities}</div>
+        <div class="me-row"><span>姓名</span>{$mprocProfile.contact_name|default=''|htmlentities}</div>
+        <div class="me-row"><span>部门</span>{$mprocProfile.department|default=''|htmlentities}</div>
+        <div class="me-row"><span>手机号</span>{$mprocProfile.phone|default=''|htmlentities}</div>
+        <div class="me-row"><span>邮箱</span>{$mprocProfile.email|default=''|htmlentities}</div>
+        <a class="btn-me-logout" href="{:url('index/index/logout')}">退出登录</a>
+    </div>
+</div>
+
+<nav class="tabbar" id="tabbar" aria-label="主导航">
+    <button type="button" class="tabbar-btn active" data-main-tab="orders">首页</button>
+    <button type="button" class="tabbar-btn" data-main-tab="me">我的</button>
+</nav>
+
+<div class="toast" id="toast"></div>
+<div class="confirm-mask" id="scoreMask">
+    <div class="confirm-box">
+        <div class="confirm-box-body">
+            <div class="confirm-ord" id="scoreOrd"></div>
+            <div class="confirm-meta" id="scoreMeta"></div>
+            <div class="score-field">
+                <span class="label">交货情况<span class="req">*</span></span>
+                <div class="score-opts">
+                    <label class="score-opt pass">
+                        <input type="radio" name="delivery_status" value="准时">
+                        <span>准时</span>
+                    </label>
+                    <label class="score-opt fail">
+                        <input type="radio" name="delivery_status" value="滞后">
+                        <span>滞后</span>
+                    </label>
+                </div>
+            </div>
+            <div class="score-field">
+                <span class="label">交货日期<span class="req">*</span></span>
+                <div class="score-date-wrap" id="deliveryDateWrap">
+                    <input type="date" class="score-date" id="deliveryDate">
+                </div>
+            </div>
+        </div>
+        <div class="confirm-acts">
+            <button type="button" class="confirm-cancel" id="scoreCancel">取消</button>
+            <button type="button" class="confirm-ok" id="scoreOk">提交</button>
+        </div>
+    </div>
+</div>
+<script>
+(function () {
+    var listUrl = "{:url('index/index/deliveryscorelist')}";
+    var saveUrl = "{:url('index/index/deliveryscoresave')}";
+    var loginUrl = "{:url('index/index/login')}";
+    var token = {:json_encode(isset($mprocBootstrapToken) ? $mprocBootstrapToken : '', JSON_UNESCAPED_UNICODE)};
+    var keepHours = {$mprocBootstrapKeepHours|default=72};
+
+    try {
+        if (token) {
+            localStorage.setItem('mproc_token', token);
+            localStorage.setItem('mproc_token_exp', String(Date.now() + keepHours * 3600 * 1000));
+        }
+    } catch (e) {}
+
+    function clearLocalLoginToken() {
+        try {
+            localStorage.removeItem('mproc_token');
+            localStorage.removeItem('mproc_token_exp');
+        } catch (e) {}
+    }
+
+    document.querySelectorAll('a.btn-me-logout, a.bar-logout').forEach(function (a) {
+        a.addEventListener('click', function () {
+            clearLocalLoginToken();
+        });
+    });
+
+    function showToast(msg, type) {
+        var el = document.getElementById('toast');
+        if (!el) return;
+        el.textContent = msg || '';
+        el.className = 'toast show' + (type === 'ok' ? ' ok' : (type === 'err' ? ' err' : ''));
+        clearTimeout(showToast._t);
+        showToast._t = setTimeout(function () { el.className = 'toast'; }, 2800);
+    }
+
+    function esc(s) {
+        return String(s == null ? '' : s)
+            .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
+            .replace(/"/g, '&quot;');
+    }
+
+    function titleText(row) {
+        var no = String(row.CCYDH || '').trim();
+        var name = String(row.CYJMC || '').trim();
+        if (no && name) return no + ' ' + name;
+        return no || name || '—';
+    }
+
+    var currentTab = 'pending';
+
+    function emptyText(tab) {
+        if (tab === 'scored') return '暂无已确认订单';
+        return '暂无待确认订单';
+    }
+
+    function renderRows(rows) {
+        var main = document.getElementById('listMain');
+        if (!main) return;
+        if (!rows || !rows.length) {
+            main.innerHTML = '<div class="empty">' + emptyText(currentTab) + '</div>';
+            return;
+        }
+        var html = '';
+        var pending = currentTab === 'pending';
+        rows.forEach(function (row) {
+            html += '<div class="card" data-ccydh="' + esc(row.CCYDH || '') + '">';
+            html += '<div class="ord">' + esc(titleText(row)) + '</div>';
+            html += '<div class="meta">';
+            html += '<div><b>供应商:</b>' + esc(row.pick_company_name || '—') + '</div>';
+            html += '<div><b>工序:</b>' + esc(row.CGYMC || '—') + '</div>';
+            html += '</div>';
+            if (pending) {
+                html += '<div class="actions"><button type="button" class="btn-score">确认交货</button></div>';
+            } else {
+                var ds = String(row.delivery_status || '');
+                var dd = String(row.delivery_date || '');
+                html += '<div class="score-summary"'
+                    + ' data-delivery-status="' + esc(ds) + '"'
+                    + ' data-delivery-date="' + esc(dd) + '">'
+                    + '<span class="res' + (ds === '滞后' ? ' is-fail' : '') + '">' + esc(ds || '—') + '</span>'
+                    + (dd ? '<span>交货日期:' + esc(dd) + '</span>' : '')
+                    + '</div>';
+                if (row.score_time) {
+                    html += '<div class="score-time">操作时间:' + esc(row.score_time) + '</div>';
+                }
+                html += '<div class="actions"><button type="button" class="btn-score">确认交货</button></div>';
+            }
+            html += '</div>';
+        });
+        main.innerHTML = html;
+    }
+
+    function postForm(url, data) {
+        var body = Object.keys(data).map(function (k) {
+            return encodeURIComponent(k) + '=' + encodeURIComponent(data[k] == null ? '' : data[k]);
+        }).join('&');
+        var headers = {
+            'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
+            'X-Requested-With': 'XMLHttpRequest'
+        };
+        try {
+            var tok = localStorage.getItem('mproc_token') || token || '';
+            if (tok) headers['X-Mproc-Token'] = tok;
+        } catch (e) {}
+        return fetch(url, { method: 'POST', credentials: 'same-origin', headers: headers, body: body })
+            .then(function (r) { return r.json(); });
+    }
+
+    var searchTimer = null;
+    function loadList() {
+        var q = (document.getElementById('qInput') || {}).value || '';
+        postForm(listUrl, { tab: currentTab, q: q }).then(function (ret) {
+            if (!ret || (ret.code !== 1 && ret.code !== '1')) {
+                if (ret && /登录/.test(String(ret.msg || ''))) {
+                    location.href = loginUrl;
+                    return;
+                }
+                showToast(ret && ret.msg ? ret.msg : '加载失败', 'err');
+                return;
+            }
+            var d = ret.data || {};
+            renderRows(d.rows || []);
+        }).catch(function () {
+            showToast('网络错误', 'err');
+        });
+    }
+
+    document.getElementById('listCats').addEventListener('click', function (e) {
+        var btn = e.target.closest('.cat[data-list-tab]');
+        if (!btn) return;
+        var tab = btn.getAttribute('data-list-tab');
+        if (!tab || tab === currentTab) return;
+        currentTab = tab;
+        var cats = document.querySelectorAll('#listCats .cat');
+        cats.forEach(function (c) { c.classList.toggle('active', c === btn); });
+        loadList();
+    });
+
+    var $qInput = document.getElementById('qInput');
+    if ($qInput) {
+        $qInput.addEventListener('input', function () {
+            clearTimeout(searchTimer);
+            searchTimer = setTimeout(loadList, 280);
+        });
+    }
+    document.getElementById('searchForm').addEventListener('submit', function (e) {
+        e.preventDefault();
+        clearTimeout(searchTimer);
+        loadList();
+    });
+
+    var paneOrders = document.getElementById('pane-orders');
+    var paneMe = document.getElementById('pane-me');
+    var barTitle = document.getElementById('barTitle');
+    document.getElementById('tabbar').addEventListener('click', function (e) {
+        var btn = e.target.closest('.tabbar-btn');
+        if (!btn) return;
+        var tab = btn.getAttribute('data-main-tab');
+        var btns = document.querySelectorAll('#tabbar .tabbar-btn');
+        btns.forEach(function (b) { b.classList.toggle('active', b === btn); });
+        if (tab === 'me') {
+            document.body.className = 'layout-me';
+            paneOrders.style.display = 'none';
+            paneMe.style.display = 'block';
+            if (barTitle) barTitle.textContent = '我的';
+        } else {
+            document.body.className = 'layout-orders';
+            paneOrders.style.display = 'flex';
+            paneMe.style.display = 'none';
+            if (barTitle) barTitle.textContent = '交货情况';
+        }
+    });
+
+    var scoreMask = document.getElementById('scoreMask');
+    var scoreOrd = document.getElementById('scoreOrd');
+    var scoreMeta = document.getElementById('scoreMeta');
+    var scorePendingCard = null;
+
+    function textOf(el) {
+        return el ? String(el.textContent || '').replace(/\s+/g, ' ').trim() : '';
+    }
+
+    function checkedVal(name) {
+        var el = document.querySelector('input[name="' + name + '"]:checked');
+        return el ? String(el.value || '') : '';
+    }
+
+    function fillScoreForm(status, date) {
+        var deliveryRadios = document.querySelectorAll('input[name="delivery_status"]');
+        deliveryRadios.forEach(function (r) {
+            r.checked = String(r.value) === String(status || '');
+        });
+        var dateEl = document.getElementById('deliveryDate');
+        if (dateEl) {
+            dateEl.value = /^\d{4}-\d{2}-\d{2}$/.test(String(date || '')) ? String(date) : '';
+        }
+    }
+
+    function resetScoreForm() {
+        fillScoreForm('', '');
+    }
+
+    function openDatePicker() {
+        var dateEl = document.getElementById('deliveryDate');
+        if (!dateEl) return;
+        try {
+            if (typeof dateEl.showPicker === 'function') {
+                dateEl.showPicker();
+                return;
+            }
+        } catch (e) {}
+        dateEl.focus();
+        try { dateEl.click(); } catch (e2) {}
+    }
+
+    function openScoreForm(card) {
+        scorePendingCard = card || null;
+        var ord = textOf(card.querySelector('.ord')) || '—';
+        var metaLines = card.querySelectorAll('.meta > div');
+        var gymc = '—';
+        var company = '—';
+        for (var i = 0; i < metaLines.length; i++) {
+            var t = textOf(metaLines[i]);
+            if (t.indexOf('工序:') === 0) gymc = t.slice(3).trim() || '—';
+            if (t.indexOf('供应商:') === 0) company = t.slice(4).trim() || '—';
+        }
+        if (scoreOrd) scoreOrd.textContent = ord;
+        if (scoreMeta) {
+            scoreMeta.innerHTML = '<div><b>供应商:</b>' + esc(company) + '</div>'
+                + '<div><b>工序:</b>' + esc(gymc) + '</div>';
+        }
+        var summary = card.querySelector('.score-summary');
+        if (summary) {
+            fillScoreForm(
+                summary.getAttribute('data-delivery-status') || '',
+                summary.getAttribute('data-delivery-date') || ''
+            );
+        } else {
+            resetScoreForm();
+        }
+        if (scoreMask) scoreMask.classList.add('show');
+    }
+
+    function closeScoreForm() {
+        scorePendingCard = null;
+        if (scoreMask) scoreMask.classList.remove('show');
+    }
+
+    document.getElementById('scoreCancel').addEventListener('click', closeScoreForm);
+    scoreMask.addEventListener('click', function (e) {
+        if (e.target === scoreMask) closeScoreForm();
+    });
+    var deliveryDateWrap = document.getElementById('deliveryDateWrap');
+    if (deliveryDateWrap) {
+        deliveryDateWrap.addEventListener('click', function (e) {
+            if (e.target && e.target.id === 'deliveryDate') return;
+            openDatePicker();
+        });
+    }
+    var deliveryDateEl = document.getElementById('deliveryDate');
+    if (deliveryDateEl) {
+        deliveryDateEl.addEventListener('click', function () {
+            openDatePicker();
+        });
+    }
+
+    function submitScore(card, payload) {
+        var ccydh = card.getAttribute('data-ccydh');
+        if (!ccydh || !payload || !payload.delivery_status) return;
+        var buttons = card.querySelectorAll('button');
+        buttons.forEach(function (b) { b.disabled = true; });
+        var okBtn = document.getElementById('scoreOk');
+        if (okBtn) okBtn.disabled = true;
+        postForm(saveUrl, {
+            ccydh: ccydh,
+            delivery_status: payload.delivery_status,
+            delivery_date: payload.delivery_date || ''
+        }).then(function (ret) {
+            if (okBtn) okBtn.disabled = false;
+            if (!ret || (ret.code !== 1 && ret.code !== '1')) {
+                buttons.forEach(function (b) { b.disabled = false; });
+                if (ret && /登录/.test(String(ret.msg || ''))) {
+                    location.href = loginUrl;
+                    return;
+                }
+                showToast(ret && ret.msg ? ret.msg : '保存失败', 'err');
+                return;
+            }
+            closeScoreForm();
+            showToast(ret.msg || '已保存', 'ok');
+            if (currentTab === 'scored') {
+                loadList();
+                return;
+            }
+            card.parentNode && card.parentNode.removeChild(card);
+            if (!document.querySelector('#listMain .card')) {
+                document.getElementById('listMain').innerHTML = '<div class="empty">暂无待确认订单</div>';
+            }
+        }).catch(function () {
+            if (okBtn) okBtn.disabled = false;
+            buttons.forEach(function (b) { b.disabled = false; });
+            showToast('网络错误', 'err');
+        });
+    }
+
+    document.getElementById('scoreOk').addEventListener('click', function () {
+        var card = scorePendingCard;
+        if (!card) return;
+        var deliveryStatus = checkedVal('delivery_status');
+        if (deliveryStatus !== '准时' && deliveryStatus !== '滞后') {
+            showToast('请选择交货情况', 'err');
+            return;
+        }
+        var deliveryDate = ((document.getElementById('deliveryDate') || {}).value || '').trim();
+        if (!/^\d{4}-\d{2}-\d{2}$/.test(deliveryDate)) {
+            showToast('请选择交货日期', 'err');
+            var dateEl = document.getElementById('deliveryDate');
+            if (dateEl) dateEl.focus();
+            return;
+        }
+        submitScore(card, { delivery_status: deliveryStatus, delivery_date: deliveryDate });
+    });
+
+    document.getElementById('listMain').addEventListener('click', function (e) {
+        var btn = e.target.closest('button.btn-score');
+        if (!btn) return;
+        var card = btn.closest('.card');
+        if (!card) return;
+        openScoreForm(card);
+    });
+})();
+</script>
+</body>
+</html>

+ 214 - 69
application/index/view/index/inboundscore.html

@@ -4,6 +4,11 @@
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
     <title>质量评分</title>
+    <meta property="og:title" content="质量评分">
+    <meta property="og:image" content="{:htmlentities(default_admin_avatar_url())}">
+    <link rel="shortcut icon" href="{:htmlentities(default_admin_avatar_url())}" type="image/png">
+    <link rel="icon" href="{:htmlentities(default_admin_avatar_url())}" type="image/png">
+    <link rel="apple-touch-icon" href="{:htmlentities(default_admin_avatar_url())}">
     <style>
         * { box-sizing: border-box; }
         html, body { margin: 0; height: 100%; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; background: #f5f5f5; color: #333; }
@@ -65,12 +70,14 @@
             background: #f0f7fb; color: #3c8dbc; font-weight: 600;
             box-shadow: inset 3px 0 0 #3c8dbc;
         }
-        .card .result-tag {
-            display: inline-block; margin-top: 10px; padding: 4px 10px;
-            border-radius: 6px; font-size: 13px; font-weight: 600; color: #fff;
+        .card .score-summary {
+            margin-top: 8px; font-size: 12px; color: #666; line-height: 1.5;
+            display: flex; flex-wrap: wrap; align-items: center; gap: 6px 10px;
         }
-        .card .result-tag.pass { background: #27ae60; }
-        .card .result-tag.fail { background: #e74c3c; }
+        .card .score-summary .res {
+            font-weight: 700; color: #27ae60;
+        }
+        .card .score-summary .res.is-fail { color: #e74c3c; }
         .card .score-extra { margin-top: 6px; font-size: 12px; color: #666; line-height: 1.5; }
         .card .score-time { margin-top: 6px; font-size: 12px; color: #888; }
         .main {
@@ -95,6 +102,9 @@
         }
         .btn-score { background: #3c8dbc; }
         .btn-score:disabled { opacity: .55; }
+        .btn-complaint { background: #e67e22; }
+        .btn-complaint:disabled { opacity: .55; }
+        .btn-complaint.is-yes { background: #c0392b; }
         .me-panel { padding: 16px 14px 24px; max-width: 480px; margin: 0 auto; }
         .me-card { background: #fff; border-radius: 12px; padding: 18px 16px; box-shadow: 0 1px 6px rgba(0,0,0,.06); }
         .me-row { font-size: 14px; line-height: 1.7; padding: 8px 0; border-bottom: 1px solid #f0f0f0; }
@@ -264,8 +274,9 @@
 
 <div id="pane-me" class="me-panel">
     <div class="me-card">
-        <div class="me-row"><span>身份</span>{$mprocProfile.company_name|default='管理员'|htmlentities}</div>
+        <div class="me-row"><span>账号</span>{$mprocProfile.account|default=''|htmlentities}</div>
         <div class="me-row"><span>姓名</span>{$mprocProfile.contact_name|default=''|htmlentities}</div>
+        <div class="me-row"><span>部门</span>{$mprocProfile.department|default=''|htmlentities}</div>
         <div class="me-row"><span>手机号</span>{$mprocProfile.phone|default=''|htmlentities}</div>
         <div class="me-row"><span>邮箱</span>{$mprocProfile.email|default=''|htmlentities}</div>
         <a class="btn-me-logout" href="{:url('index/index/logout')}">退出登录</a>
@@ -273,7 +284,7 @@
 </div>
 
 <nav class="tabbar" id="tabbar" aria-label="主导航">
-    <button type="button" class="tabbar-btn active" data-main-tab="orders">订单</button>
+    <button type="button" class="tabbar-btn active" data-main-tab="orders">首页</button>
     <button type="button" class="tabbar-btn" data-main-tab="me">我的</button>
 </nav>
 
@@ -297,52 +308,55 @@
                 </div>
             </div>
             <div class="score-field">
-                <span class="label">交货情况<span class="req">*</span></span>
-                <div class="score-opts">
-                    <label class="score-opt pass">
-                        <input type="radio" name="delivery_status" value="准时">
-                        <span>准时</span>
-                    </label>
-                    <label class="score-opt fail">
-                        <input type="radio" name="delivery_status" value="滞后">
-                        <span>滞后</span>
-                    </label>
-                </div>
-            </div>
-            <div class="score-field">
-                <span class="label">客户是否投诉<span class="req">*</span></span>
+                <span class="label">订单是否中断<span class="req">*</span></span>
                 <div class="score-opts">
                     <label class="score-opt">
-                        <input type="radio" name="customer_complaint" value="0" checked>
+                        <input type="radio" name="order_interrupt" value="0" checked>
                         <span>否</span>
                     </label>
                     <label class="score-opt">
-                        <input type="radio" name="customer_complaint" value="1">
+                        <input type="radio" name="order_interrupt" value="1">
                         <span>是</span>
                     </label>
                 </div>
             </div>
+            <div class="score-field score-field-remark">
+                <span class="label">备注<span class="req" id="remarkReq" style="display:none;">*</span></span>
+                <textarea class="score-remark" id="scoreRemark" maxlength="500" placeholder=""></textarea>
+            </div>
+        </div>
+        <div class="confirm-acts">
+            <button type="button" class="confirm-cancel" id="scoreCancel">取消</button>
+            <button type="button" class="confirm-ok" id="scoreOk">提交</button>
+        </div>
+    </div>
+</div>
+<div class="confirm-mask" id="complaintMask">
+    <div class="confirm-box">
+        <div class="confirm-box-body">
+            <div class="confirm-ord" id="complaintOrd"></div>
+            <div class="confirm-meta" id="complaintMeta"></div>
             <div class="score-field">
-                <span class="label">订单是否中断<span class="req">*</span></span>
+                <span class="label">客户是否投诉<span class="req">*</span></span>
                 <div class="score-opts">
                     <label class="score-opt">
-                        <input type="radio" name="order_interrupt" value="0" checked>
+                        <input type="radio" name="complaint_value" value="0">
                         <span>否</span>
                     </label>
-                    <label class="score-opt">
-                        <input type="radio" name="order_interrupt" value="1">
+                    <label class="score-opt fail">
+                        <input type="radio" name="complaint_value" value="1">
                         <span>是</span>
                     </label>
                 </div>
             </div>
-            <div class="score-field score-field-remark">
-                <span class="label">备注<span class="req" id="remarkReq" style="display:none;">*</span></span>
-                <textarea class="score-remark" id="scoreRemark" maxlength="500" placeholder=""></textarea>
+            <div class="score-field score-field-remark" id="complaintRemarkWrap">
+                <span class="label">投诉说明<span class="req" id="complaintRemarkReq" style="display:none;">*</span></span>
+                <textarea class="score-remark" id="complaintRemark" maxlength="500" placeholder="请说明投诉原因"></textarea>
             </div>
         </div>
         <div class="confirm-acts">
-            <button type="button" class="confirm-cancel" id="scoreCancel">取消</button>
-            <button type="button" class="confirm-ok" id="scoreOk">提交</button>
+            <button type="button" class="confirm-cancel" id="complaintCancel">取消</button>
+            <button type="button" class="confirm-ok" id="complaintOk">提交</button>
         </div>
     </div>
 </div>
@@ -350,6 +364,7 @@
 (function () {
     var listUrl = "{:url('index/index/inboundscorelist')}";
     var saveUrl = "{:url('index/index/inboundscoresave')}";
+    var complaintUrl = "{:url('index/index/inboundscorecomplaint')}";
     var loginUrl = "{:url('index/index/login')}";
     var token = {:json_encode(isset($mprocBootstrapToken) ? $mprocBootstrapToken : '', JSON_UNESCAPED_UNICODE)};
     var keepHours = {$mprocBootstrapKeepHours|default=72};
@@ -427,17 +442,36 @@
                 html += '<div class="actions"><button type="button" class="btn-score">评分</button></div>';
             } else {
                 var res = String(row.result || '');
-                var cls = res === '不合格' ? 'fail' : 'pass';
-                html += '<span class="result-tag ' + cls + '">' + esc(res || '—') + '</span>';
-                html += '<div class="score-extra">交货情况:' + esc(row.delivery_status || '—')
-                    + ' 客户投诉:' + esc(ynText(row.customer_complaint))
-                    + ' 订单中断:' + esc(ynText(row.order_interrupt)) + '</div>';
-                if (row.remark) {
-                    html += '<div class="score-extra">备注:' + esc(row.remark) + '</div>';
+                var interrupt = parseInt(row.order_interrupt, 10) === 1 ? '1' : '0';
+                var complaint = parseInt(row.customer_complaint, 10) === 1 ? '1' : '0';
+                var complained = complaint === '1';
+                var remark = String(row.remark || '');
+                var complaintRemark = String(row.complaint_remark || '');
+                html += '<div class="score-summary"'
+                    + ' data-result="' + esc(res) + '"'
+                    + ' data-interrupt="' + interrupt + '"'
+                    + ' data-complaint="' + complaint + '"'
+                    + ' data-complaint-remark="' + esc(complaintRemark) + '"'
+                    + ' data-remark="' + esc(remark) + '">'
+                    + '<span class="res' + (res === '不合格' ? ' is-fail' : '') + '">' + esc(res || '—') + '</span>'
+                    + '<span>客户投诉:' + esc(ynText(complaint)) + '</span>'
+                    + '<span>订单中断:' + esc(ynText(interrupt)) + '</span>'
+                    + '</div>';
+                if (complained && complaintRemark) {
+                    html += '<div class="score-extra">投诉说明:' + esc(complaintRemark) + '</div>';
+                }
+                if (remark) {
+                    html += '<div class="score-extra">备注:' + esc(remark) + '</div>';
                 }
                 if (row.score_time) {
-                    html += '<div class="score-time">评定时间:' + esc(row.score_time) + '</div>';
+                    html += '<div class="score-time">操作时间:' + esc(row.score_time) + '</div>';
                 }
+                html += '<div class="actions">'
+                    + '<button type="button" class="btn-score">评分</button>'
+                    + '<button type="button" class="btn-complaint' + (complained ? ' is-yes' : '') + '" data-complaint="'
+                    + complaint + '">'
+                    + (complained ? '已投诉·修改' : '登记客诉')
+                    + '</button></div>';
             }
             html += '</div>';
         });
@@ -550,20 +584,25 @@
         }
     }
 
-    function resetScoreForm() {
+    function fillScoreForm(result, interrupt, remark) {
         var resultRadios = document.querySelectorAll('input[name="score_result"]');
-        resultRadios.forEach(function (r) { r.checked = false; });
-        var deliveryRadios = document.querySelectorAll('input[name="delivery_status"]');
-        deliveryRadios.forEach(function (r) { r.checked = false; });
-        var complaintNo = document.querySelector('input[name="customer_complaint"][value="0"]');
-        var interruptNo = document.querySelector('input[name="order_interrupt"][value="0"]');
-        if (complaintNo) complaintNo.checked = true;
-        if (interruptNo) interruptNo.checked = true;
+        resultRadios.forEach(function (r) {
+            r.checked = String(r.value) === String(result || '');
+        });
+        var interruptVal = String(interrupt) === '1' ? '1' : '0';
+        var interruptRadios = document.querySelectorAll('input[name="order_interrupt"]');
+        interruptRadios.forEach(function (r) {
+            r.checked = String(r.value) === interruptVal;
+        });
         var remarkEl = document.getElementById('scoreRemark');
-        if (remarkEl) remarkEl.value = '';
+        if (remarkEl) remarkEl.value = remark || '';
         syncRemarkRequired();
     }
 
+    function resetScoreForm() {
+        fillScoreForm('', '0', '');
+    }
+
     function openScoreForm(card) {
         scorePendingCard = card || null;
         var ord = textOf(card.querySelector('.ord')) || '—';
@@ -580,7 +619,16 @@
             scoreMeta.innerHTML = '<div><b>供应商:</b>' + esc(company) + '</div>'
                 + '<div><b>工序:</b>' + esc(gymc) + '</div>';
         }
-        resetScoreForm();
+        var summary = card.querySelector('.score-summary');
+        if (summary) {
+            fillScoreForm(
+                summary.getAttribute('data-result') || '',
+                summary.getAttribute('data-interrupt') || '0',
+                summary.getAttribute('data-remark') || ''
+            );
+        } else {
+            resetScoreForm();
+        }
         if (scoreMask) scoreMask.classList.add('show');
     }
 
@@ -607,8 +655,6 @@
         postForm(saveUrl, {
             ccydh: ccydh,
             result: payload.result,
-            delivery_status: payload.delivery_status,
-            customer_complaint: payload.customer_complaint,
             order_interrupt: payload.order_interrupt,
             remark: payload.remark || ''
         }).then(function (ret) {
@@ -624,6 +670,10 @@
             }
             closeScoreForm();
             showToast(ret.msg || '已保存', 'ok');
+            if (currentTab === 'scored') {
+                loadList();
+                return;
+            }
             card.parentNode && card.parentNode.removeChild(card);
             if (!document.querySelector('#listMain .card')) {
                 document.getElementById('listMain').innerHTML = '<div class="empty">暂无待评分订单</div>';
@@ -643,17 +693,7 @@
             showToast('请选择是否合格', 'err');
             return;
         }
-        var deliveryStatus = checkedVal('delivery_status');
-        if (deliveryStatus !== '准时' && deliveryStatus !== '滞后') {
-            showToast('请选择交货情况', 'err');
-            return;
-        }
-        var complaint = checkedVal('customer_complaint');
         var interrupt = checkedVal('order_interrupt');
-        if (complaint !== '0' && complaint !== '1') {
-            showToast('请选择客户是否投诉', 'err');
-            return;
-        }
         if (interrupt !== '0' && interrupt !== '1') {
             showToast('请选择订单是否中断', 'err');
             return;
@@ -667,19 +707,124 @@
         }
         submitScore(card, {
             result: result,
-            delivery_status: deliveryStatus,
-            customer_complaint: complaint,
             order_interrupt: interrupt,
             remark: remark
         });
     });
 
-    document.getElementById('listMain').addEventListener('click', function (e) {
-        var btn = e.target.closest('button.btn-score');
-        if (!btn) return;
-        var card = btn.closest('.card');
+    var complaintMask = document.getElementById('complaintMask');
+    var complaintPendingCard = null;
+
+    function syncComplaintRemarkRequired() {
+        var val = checkedVal('complaint_value');
+        var reqEl = document.getElementById('complaintRemarkReq');
+        if (reqEl) reqEl.style.display = val === '1' ? '' : 'none';
+    }
+
+    function openComplaintForm(card) {
+        complaintPendingCard = card || null;
+        var ord = textOf(card.querySelector('.ord')) || '—';
+        var metaLines = card.querySelectorAll('.meta > div');
+        var gymc = '—';
+        var company = '—';
+        for (var i = 0; i < metaLines.length; i++) {
+            var t = textOf(metaLines[i]);
+            if (t.indexOf('工序:') === 0) gymc = t.slice(3).trim() || '—';
+            if (t.indexOf('供应商:') === 0) company = t.slice(4).trim() || '—';
+        }
+        var complaintOrd = document.getElementById('complaintOrd');
+        var complaintMeta = document.getElementById('complaintMeta');
+        if (complaintOrd) complaintOrd.textContent = ord;
+        if (complaintMeta) {
+            complaintMeta.innerHTML = '<div><b>供应商:</b>' + esc(company) + '</div>'
+                + '<div><b>工序:</b>' + esc(gymc) + '</div>';
+        }
+        var summary = card.querySelector('.score-summary');
+        var cur = card.querySelector('.btn-complaint');
+        var curVal = summary
+            ? String(summary.getAttribute('data-complaint') || '0')
+            : (cur ? String(cur.getAttribute('data-complaint') || '0') : '0');
+        var radios = document.querySelectorAll('input[name="complaint_value"]');
+        radios.forEach(function (r) {
+            r.checked = String(r.value) === curVal;
+        });
+        var remarkEl = document.getElementById('complaintRemark');
+        if (remarkEl) {
+            remarkEl.value = summary ? String(summary.getAttribute('data-complaint-remark') || '') : '';
+        }
+        syncComplaintRemarkRequired();
+        if (complaintMask) complaintMask.classList.add('show');
+    }
+
+    function closeComplaintForm() {
+        complaintPendingCard = null;
+        if (complaintMask) complaintMask.classList.remove('show');
+    }
+
+    document.getElementById('complaintCancel').addEventListener('click', closeComplaintForm);
+    if (complaintMask) {
+        complaintMask.addEventListener('click', function (e) {
+            if (e.target === complaintMask) closeComplaintForm();
+        });
+    }
+    document.querySelectorAll('input[name="complaint_value"]').forEach(function (r) {
+        r.addEventListener('change', syncComplaintRemarkRequired);
+    });
+    document.getElementById('complaintOk').addEventListener('click', function () {
+        var card = complaintPendingCard;
         if (!card) return;
-        openScoreForm(card);
+        var val = checkedVal('complaint_value');
+        if (val !== '0' && val !== '1') {
+            showToast('请选择客户是否投诉', 'err');
+            return;
+        }
+        var remark = ((document.getElementById('complaintRemark') || {}).value || '').trim();
+        if (val === '1' && !remark) {
+            showToast('请填写投诉说明', 'err');
+            var remarkEl = document.getElementById('complaintRemark');
+            if (remarkEl) remarkEl.focus();
+            return;
+        }
+        if (val !== '1') remark = '';
+        var ccydh = card.getAttribute('data-ccydh');
+        if (!ccydh) return;
+        var okBtn = document.getElementById('complaintOk');
+        if (okBtn) okBtn.disabled = true;
+        postForm(complaintUrl, {
+            ccydh: ccydh,
+            customer_complaint: val,
+            complaint_remark: remark
+        }).then(function (ret) {
+            if (okBtn) okBtn.disabled = false;
+            if (!ret || (ret.code !== 1 && ret.code !== '1')) {
+                if (ret && /登录/.test(String(ret.msg || ''))) {
+                    location.href = loginUrl;
+                    return;
+                }
+                showToast(ret && ret.msg ? ret.msg : '保存失败', 'err');
+                return;
+            }
+            closeComplaintForm();
+            showToast(ret.msg || '已保存', 'ok');
+            loadList();
+        }).catch(function () {
+            if (okBtn) okBtn.disabled = false;
+            showToast('网络错误', 'err');
+        });
+    });
+
+    document.getElementById('listMain').addEventListener('click', function (e) {
+        var scoreBtn = e.target.closest('button.btn-score');
+        if (scoreBtn) {
+            var scoreCard = scoreBtn.closest('.card');
+            if (scoreCard) openScoreForm(scoreCard);
+            return;
+        }
+        var complaintBtn = e.target.closest('button.btn-complaint');
+        if (complaintBtn) {
+            var complaintCard = complaintBtn.closest('.card');
+            if (complaintCard) openComplaintForm(complaintCard);
+        }
     });
 })();
 </script>

+ 8 - 2
application/index/view/index/index.html

@@ -4,6 +4,11 @@
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
     <title>报价列表</title>
+    <meta property="og:title" content="报价列表">
+    <meta property="og:image" content="{:htmlentities(default_admin_avatar_url())}">
+    <link rel="shortcut icon" href="{:htmlentities(default_admin_avatar_url())}" type="image/png">
+    <link rel="icon" href="{:htmlentities(default_admin_avatar_url())}" type="image/png">
+    <link rel="apple-touch-icon" href="{:htmlentities(default_admin_avatar_url())}">
     <style>
         * { box-sizing: border-box; }
         html {
@@ -843,8 +848,9 @@
 
 <div id="mproc-pane-me" class="me-panel" style="{eq name='mprocMainTab' value='me'}display:block{else/}display:none{/eq}">
     <div class="me-card">
-        <div class="me-row"><span>公司名称</span>{$mprocProfile.company_name|default=''|htmlentities}</div>
+        <div class="me-row"><span>账号</span>{$mprocProfile.account|default=''|htmlentities}</div>
         <div class="me-row"><span>姓名</span>{$mprocProfile.contact_name|default=''|htmlentities}</div>
+        <div class="me-row"><span>部门</span>{$mprocProfile.department|default=''|htmlentities}</div>
         <div class="me-row"><span>手机号</span>{$mprocProfile.phone|default=''|htmlentities}</div>
         <div class="me-row"><span>邮箱</span>{$mprocProfile.email|default=''|htmlentities}</div>
         {if $mprocCanChangePwd}
@@ -854,7 +860,7 @@
 </div>
 
 <nav class="tabbar" id="mproc-tabbar" aria-label="主导航">
-    <button type="button" class="tabbar-btn {eq name='mprocMainTab' value='orders'}active{/eq}" data-main-tab="orders">订单</button>
+    <button type="button" class="tabbar-btn {eq name='mprocMainTab' value='orders'}active{/eq}" data-main-tab="orders">首页</button>
     <button type="button" class="tabbar-btn {eq name='mprocMainTab' value='me'}active{/eq}" data-main-tab="me">我的</button>
 </nav>
 

+ 8 - 2
application/index/view/index/login.html

@@ -3,7 +3,13 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
-    <title>协助明细登录</title>
+    <title>登录</title>
+    <meta property="og:title" content="登录">
+    <meta property="og:image" content="{:htmlentities(default_admin_avatar_url())}">
+    <meta name="twitter:image" content="{:htmlentities(default_admin_avatar_url())}">
+    <link rel="shortcut icon" href="{:htmlentities(default_admin_avatar_url())}" type="image/png">
+    <link rel="icon" href="{:htmlentities(default_admin_avatar_url())}" type="image/png">
+    <link rel="apple-touch-icon" href="{:htmlentities(default_admin_avatar_url())}">
     <style>
         * { box-sizing: border-box; }
         html, body { margin: 0; height: 100%; width: 100%; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; background: #f0f2f5; }
@@ -123,7 +129,7 @@
         <div id="panel-account" class="panel active">
             <div class="field">
                 <label for="username">账号</label>
-                <input type="text" id="username" name="username" maxlength="11" placeholder="登录账号(手机号)" autocomplete="username">
+                <input type="text" id="username" name="username" maxlength="64" placeholder="账号" autocomplete="username">
             </div>
             <div class="field">
                 <label for="password">密码</label>

+ 177 - 42
public/assets/js/backend/procuremen.js

@@ -5708,52 +5708,144 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                     return found;
                 }
 
-                function auditIssueRenderBidOpenUserOptions(excludeId, selectedId) {
-                    var opts = '<option value="">请选择</option>';
+                function auditIssueFindUser(uid) {
+                    uid = parseInt(uid, 10) || 0;
+                    var hit = null;
+                    $.each(users, function (i, u) {
+                        if ((parseInt(u.id, 10) || 0) === uid) {
+                            hit = u;
+                            return false;
+                        }
+                    });
+                    return hit;
+                }
+
+                function auditIssueBidOpenUserLabel(u) {
+                    if (!u) {
+                        return '';
+                    }
+                    var uid = parseInt(u.id, 10) || 0;
+                    var label = (u.nickname || u.username || ('ID' + uid));
+                    if (u.group_name || u.mobile) {
+                        label += '(';
+                        if (u.group_name) {
+                            label += u.group_name;
+                        }
+                        if (u.group_name && u.mobile) {
+                            label += ' · ';
+                        }
+                        if (u.mobile) {
+                            label += u.mobile;
+                        }
+                        label += ')';
+                    }
+                    return label;
+                }
+
+                function auditIssueBidOpenUserMatch(u, keyword) {
+                    keyword = $.trim(String(keyword || ''));
+                    if (!keyword) {
+                        return true;
+                    }
+                    var kw = keyword.toLowerCase();
+                    var kwDigits = keyword.replace(/\D/g, '');
+                    var name = String(u.nickname || '').toLowerCase();
+                    var username = String(u.username || '').toLowerCase();
+                    var mobilePlain = String(u.mobile_plain || u.mobile || '').replace(/\D/g, '');
+                    var mobileMask = String(u.mobile || '').toLowerCase();
+                    if (name.indexOf(kw) >= 0 || username.indexOf(kw) >= 0) {
+                        return true;
+                    }
+                    if (mobileMask.indexOf(kw) >= 0) {
+                        return true;
+                    }
+                    if (kwDigits && mobilePlain && mobilePlain.indexOf(kwDigits) >= 0) {
+                        return true;
+                    }
+                    return false;
+                }
+
+                function auditIssueGetBidOpenUserId(slot) {
+                    return parseInt($('#bid-open-user-' + slot).val(), 10) || 0;
+                }
+
+                function auditIssueSetBidOpenUser(slot, uid) {
+                    uid = parseInt(uid, 10) || 0;
+                    var $picker = $('.bid-open-picker[data-slot="' + slot + '"]');
+                    var $hidden = $('#bid-open-user-' + slot);
+                    var $search = $picker.find('.bid-open-search');
+                    $hidden.val(uid > 0 ? String(uid) : '');
+                    if (uid > 0) {
+                        $search.val(auditIssueBidOpenUserLabel(auditIssueFindUser(uid)));
+                    } else {
+                        $search.val('');
+                    }
+                    $picker.find('.bid-open-dropdown').removeClass('open');
+                }
+
+                function auditIssueRenderBidOpenDropdown(slot, keyword) {
+                    var excludeId = slot === 1 ? auditIssueGetBidOpenUserId(2) : auditIssueGetBidOpenUserId(1);
+                    var selectedId = auditIssueGetBidOpenUserId(slot);
+                    var $list = $('.bid-open-picker[data-slot="' + slot + '"] .bid-open-dropdown-list');
+                    if (!$list.length) {
+                        return;
+                    }
+                    var htmlOpts = '';
                     $.each(users, function (i, u) {
                         var uid = parseInt(u.id, 10) || 0;
-                        if (!uid) {
+                        if (!uid || (excludeId && uid === excludeId)) {
                             return;
                         }
-                        if (excludeId && uid === parseInt(excludeId, 10)) {
+                        if (!auditIssueBidOpenUserMatch(u, keyword)) {
                             return;
                         }
-                        var label = (u.nickname || u.username || ('ID' + uid));
-                        if (u.group_name || u.mobile) {
-                            label += '(';
-                            if (u.group_name) {
-                                label += u.group_name;
-                            }
-                            if (u.group_name && u.mobile) {
-                                label += ' · ';
-                            }
-                            if (u.mobile) {
-                                label += u.mobile;
+                        var active = selectedId && uid === selectedId ? ' active' : '';
+                        htmlOpts += '<button type="button" class="bid-open-option' + active + '" data-id="' + uid + '">'
+                            + $('<div>').text(auditIssueBidOpenUserLabel(u)).html()
+                            + '</button>';
+                    });
+                    if (!htmlOpts) {
+                        htmlOpts = '<div class="bid-open-empty">无匹配验证人</div>';
+                    }
+                    $list.html(htmlOpts);
+                }
+
+                function auditIssueOpenBidOpenDropdown(slot) {
+                    $('.bid-open-picker').each(function () {
+                        var s = parseInt($(this).attr('data-slot'), 10) || 0;
+                        var $dd = $(this).find('.bid-open-dropdown');
+                        if (s === slot) {
+                            var kw = $.trim($(this).find('.bid-open-search').val() || '');
+                            var selected = auditIssueGetBidOpenUserId(slot);
+                            if (selected && kw === auditIssueBidOpenUserLabel(auditIssueFindUser(selected))) {
+                                kw = '';
                             }
-                            label += ')';
+                            auditIssueRenderBidOpenDropdown(slot, kw);
+                            $dd.addClass('open');
+                        } else {
+                            $dd.removeClass('open');
                         }
-                        var selected = selectedId && uid === parseInt(selectedId, 10) ? ' selected="selected"' : '';
-                        opts += '<option value="' + uid + '"' + selected + '>' + $('<div>').text(label).html() + '</option>';
                     });
-                    return opts;
+                }
+
+                function auditIssueCloseBidOpenDropdowns() {
+                    $('.bid-open-picker .bid-open-dropdown').removeClass('open');
                 }
 
                 function auditIssueSyncBidOpenUserSelects(changedSlot) {
-                    var v1 = parseInt($('#bid-open-user-1').val(), 10) || 0;
-                    var v2 = parseInt($('#bid-open-user-2').val(), 10) || 0;
+                    var v1 = auditIssueGetBidOpenUserId(1);
+                    var v2 = auditIssueGetBidOpenUserId(2);
                     if (v1 && v2 && v1 === v2) {
                         if (changedSlot === 1) {
-                            v2 = 0;
-                            $('#bid-open-user-2').val('');
+                            auditIssueSetBidOpenUser(2, 0);
                             $('#bid-open-code-2').val('');
+                            v2 = 0;
                         } else {
-                            v1 = 0;
-                            $('#bid-open-user-1').val('');
+                            auditIssueSetBidOpenUser(1, 0);
                             $('#bid-open-code-1').val('');
+                            v1 = 0;
                         }
                     }
-                    $('#bid-open-user-1').html(auditIssueRenderBidOpenUserOptions(v2, v1));
-                    $('#bid-open-user-2').html(auditIssueRenderBidOpenUserOptions(v1, v2));
                     auditIssueSaveLastBidOpenUsers(v1, v2);
                 }
 
@@ -5764,8 +5856,8 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                     if (v1 && v2 && v1 === v2) {
                         v2 = 0;
                     }
-                    $('#bid-open-user-1').html(auditIssueRenderBidOpenUserOptions(v2, v1));
-                    $('#bid-open-user-2').html(auditIssueRenderBidOpenUserOptions(v1, v2));
+                    auditIssueSetBidOpenUser(1, v1);
+                    auditIssueSetBidOpenUser(2, v2);
                 }
 
                 var html = [
@@ -5773,13 +5865,21 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                     '<p class="bid-open-tip"><i class="fa fa-info-circle"></i> 请选择两名用户,分别发送验证码并填写。验证通过后可查看供应商报价信息。</p>',
                     '<div class="bid-open-row bid-open-row-1">',
                     '<label>验证人1</label>',
-                    '<select class="form-control bid-open-user" id="bid-open-user-1"><option value="">请选择</option></select>',
+                    '<div class="bid-open-picker" data-slot="1">',
+                    '<input type="hidden" id="bid-open-user-1" value=""/>',
+                    '<input type="text" class="form-control bid-open-search" placeholder="请选择" autocomplete="off"/>',
+                    '<div class="bid-open-dropdown"><div class="bid-open-dropdown-list"></div></div>',
+                    '</div>',
                     '<button type="button" class="btn btn-default btn-sm btn-send-code" data-slot="1">发送验证码</button>',
                     '<input type="text" class="form-control bid-open-code" id="bid-open-code-1" maxlength="6" placeholder="6位验证码"/>',
                     '</div>',
                     '<div class="bid-open-row bid-open-row-2">',
                     '<label>验证人2</label>',
-                    '<select class="form-control bid-open-user" id="bid-open-user-2"><option value="">请选择</option></select>',
+                    '<div class="bid-open-picker" data-slot="2">',
+                    '<input type="hidden" id="bid-open-user-2" value=""/>',
+                    '<input type="text" class="form-control bid-open-search" placeholder="请选择" autocomplete="off"/>',
+                    '<div class="bid-open-dropdown"><div class="bid-open-dropdown-list"></div></div>',
+                    '</div>',
                     '<button type="button" class="btn btn-default btn-sm btn-send-code" data-slot="2">发送验证码</button>',
                     '<input type="text" class="form-control bid-open-code" id="bid-open-code-2" maxlength="6" placeholder="6位验证码"/>',
                     '</div>',
@@ -5788,12 +5888,18 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                 var layerIdx = Layer.open({
                     type: 1,
                     title: '开标验证',
-                    area: ['640px', 'auto'],
+                    area: ['680px', 'auto'],
                     content: html,
                     btn: ['提交验证', '取消'],
+                    success: function (layero) {
+                        try {
+                            layero.find('.layui-layer-content').css('overflow', 'visible');
+                        } catch (e) {
+                        }
+                    },
                     yes: function (idx) {
-                        var v1 = parseInt($('#bid-open-user-1').val(), 10) || 0;
-                        var v2 = parseInt($('#bid-open-user-2').val(), 10) || 0;
+                        var v1 = auditIssueGetBidOpenUserId(1);
+                        var v2 = auditIssueGetBidOpenUserId(2);
                         var c1 = $.trim($('#bid-open-code-1').val());
                         var c2 = $.trim($('#bid-open-code-2').val());
                         if (!v1 || !v2) {
@@ -5845,20 +5951,49 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                     }
                     users = payload.users || [];
                     auditIssueApplyLastBidOpenUsers();
-                    $(document).off('change.bidOpenUser', '#bid-open-user-1, #bid-open-user-2')
-                        .on('change.bidOpenUser', '#bid-open-user-1', function () {
-                            auditIssueResetBidOpenSendBtn($('.btn-send-code[data-slot="1"]'));
-                            auditIssueSyncBidOpenUserSelects(1);
+                    $(document).off('.bidOpenPicker')
+                        .on('focus.bidOpenPicker click.bidOpenPicker', '.bid-open-search', function (e) {
+                            e.stopPropagation();
+                            var slot = parseInt($(this).closest('.bid-open-picker').attr('data-slot'), 10) || 0;
+                            if (!slot) {
+                                return;
+                            }
+                            auditIssueOpenBidOpenDropdown(slot);
+                        })
+                        .on('input.bidOpenPicker', '.bid-open-search', function () {
+                            var $picker = $(this).closest('.bid-open-picker');
+                            var slot = parseInt($picker.attr('data-slot'), 10) || 0;
+                            if (!slot) {
+                                return;
+                            }
+                            $('#bid-open-user-' + slot).val('');
+                            auditIssueRenderBidOpenDropdown(slot, $(this).val());
+                            $picker.find('.bid-open-dropdown').addClass('open');
+                        })
+                        .on('click.bidOpenPicker', '.bid-open-option', function (e) {
+                            e.preventDefault();
+                            e.stopPropagation();
+                            var $picker = $(this).closest('.bid-open-picker');
+                            var slot = parseInt($picker.attr('data-slot'), 10) || 0;
+                            var uid = parseInt($(this).attr('data-id'), 10) || 0;
+                            if (!slot || !uid) {
+                                return;
+                            }
+                            auditIssueResetBidOpenSendBtn($('.btn-send-code[data-slot="' + slot + '"]'));
+                            auditIssueSetBidOpenUser(slot, uid);
+                            auditIssueSyncBidOpenUserSelects(slot);
+                        })
+                        .on('click.bidOpenPicker', function () {
+                            auditIssueCloseBidOpenDropdowns();
                         })
-                        .on('change.bidOpenUser', '#bid-open-user-2', function () {
-                            auditIssueResetBidOpenSendBtn($('.btn-send-code[data-slot="2"]'));
-                            auditIssueSyncBidOpenUserSelects(2);
+                        .on('click.bidOpenPicker', '.audit-bid-open-modal', function (e) {
+                            e.stopPropagation();
                         });
                     return false;
                 });
                 $(document).off('click.bidOpenSend', '.btn-send-code').on('click.bidOpenSend', '.btn-send-code', function () {
                     var slot = $(this).attr('data-slot');
-                    var adminId = parseInt($('#bid-open-user-' + slot).val(), 10) || 0;
+                    var adminId = auditIssueGetBidOpenUserId(slot);
                     if (!adminId) {
                         Toastr.warning('请先选择验证人' + slot);
                         return;

+ 3 - 3
public/assets/js/backend/procuremenarchive.js

@@ -107,7 +107,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                             width: 100,
                             align: 'center',
                             formatter: function (value) {
-                                var v = value == null ? '' : String(value);
+                                var v = value == null ? '' : String(value).trim();
                                 if (v === '合格') {
                                     return '<span class="label label-success">合格</span>';
                                 }
@@ -117,12 +117,12 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                                 if (v.indexOf('合格') >= 0 || v.indexOf('不合格') >= 0) {
                                     return archiveCellText(v);
                                 }
-                                return v ? archiveCellText(v) : '';
+                                return '<span class="text-muted">—</span>';
                             }
                         },
                         {
                             field: 'createtime',
-                            title: '完结时间',
+                            title: '审批时间',
                             operate: 'RANGE',
                             addclass: 'datetimerange',
                             autocomplete: false,

+ 17 - 2
public/assets/js/backend/supplierservicescore.js

@@ -271,7 +271,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                     },
                     {
                         field: 'score_grade',
-                        title: '评等级',
+                        title: '评等级',
                         operate: false,
                         sortable: false,
                         align: 'center',
@@ -385,7 +385,22 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                 if ($ymInput.length) {
                     $ymInput.val(ym);
                 }
-                window.location.href = Fast.api.fixurl('supplierservicescore/export') + '?ym=' + encodeURIComponent(ym);
+                var $btn = $(this);
+                if ($btn.data('exporting')) {
+                    return false;
+                }
+                $btn.data('exporting', 1).addClass('disabled');
+                Toastr.info('正在导出,请稍候…');
+                var url = Fast.api.fixurl('supplierservicescore/export') + '?ym=' + encodeURIComponent(ym);
+                // 隐藏 iframe 触发下载,避免整页跳转感觉卡住
+                var $frame = $('#supplier-score-export-frame');
+                if (!$frame.length) {
+                    $frame = $('<iframe id="supplier-score-export-frame" style="display:none;width:0;height:0;border:0;"></iframe>').appendTo('body');
+                }
+                $frame.attr('src', url);
+                setTimeout(function () {
+                    $btn.data('exporting', 0).removeClass('disabled');
+                }, 2500);
                 return false;
             });
         },

برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است