m0_70156489 2 дней назад
Родитель
Сommit
c0c8caa022
39 измененных файлов с 6154 добавлено и 624 удалено
  1. 776 50
      application/admin/controller/Procuremen.php
  2. 30 1
      application/admin/controller/Procuremenarchive.php
  3. 1 0
      application/admin/controller/Procuremenmenu.php
  4. 15 22
      application/admin/controller/Supplierscorerule.php
  5. 496 42
      application/admin/controller/Supplierservicescore.php
  6. 14 9
      application/admin/model/Purchasesmstemplate.php
  7. 45 36
      application/admin/view/dashboard/index.html
  8. 102 6
      application/admin/view/procuremen/add.html
  9. 97 17
      application/admin/view/procuremen/audit_issue.html
  10. 9 3
      application/admin/view/procuremen/details_fragment.html
  11. 22 11
      application/admin/view/procuremen/outward_detail.html
  12. 17 0
      application/admin/view/procuremen/review.html
  13. 187 5
      application/admin/view/procuremen/rfqlist.html
  14. 1 1
      application/admin/view/procuremensms/edit.html
  15. 13 23
      application/admin/view/procuremensms/index.html
  16. 11 6
      application/admin/view/supplierscorerule/add.html
  17. 9 4
      application/admin/view/supplierscorerule/edit.html
  18. 2 2
      application/admin/view/supplierscorerule/index.html
  19. 2 1
      application/admin/view/supplierservicescore/index.html
  20. 124 0
      application/admin/view/supplierservicescore/monthorders.html
  21. 49 0
      application/api/controller/Procuremen.php
  22. 25 5
      application/common.php
  23. 271 10
      application/common/library/ProcuremenDashboard.php
  24. 82 1
      application/common/library/ProcuremenOperLog.php
  25. 904 97
      application/common/library/ProcuremenSupplierScore.php
  26. 6 2
      application/extra/mproc.php
  27. 21 0
      application/extra/purchase_order_inbound_score_install.sql
  28. 3 0
      application/extra/purchase_order_notify_salesman_install.sql
  29. 25 0
      application/extra/rfq_salesman_email_template_install.sql
  30. 38 0
      application/extra/supplier_score_rule_name_install.sql
  31. 2 0
      application/extra/supplier_service_final_score.sql
  32. 793 62
      application/index/controller/Index.php
  33. 488 0
      application/index/view/index/inboundscore.html
  34. 124 44
      application/index/view/index/index.html
  35. 963 63
      public/assets/js/backend/procuremen.js
  36. 21 1
      public/assets/js/backend/procuremenarchive.js
  37. 150 78
      public/assets/js/backend/procuremensms.js
  38. 11 7
      public/assets/js/backend/supplierscorerule.js
  39. 205 15
      public/assets/js/backend/supplierservicescore.js

Разница между файлами не показана из-за своего большого размера
+ 776 - 50
application/admin/controller/Procuremen.php


+ 30 - 1
application/admin/controller/Procuremenarchive.php

@@ -133,8 +133,30 @@ class Procuremenarchive extends Backend
             }
             $completeTsMap = ProcuremenTime::loadOperLogTimestampMap(
                 array_keys($sidList),
-                ['purchase_confirm', 'mark_complete']
+                ['purchase_confirm', 'mark_complete', 'inbound_score']
             );
+            $inboundMap = [];
+            if ($sidList !== []) {
+                try {
+                    $inRows = Db::table('purchase_order_inbound_score')
+                        ->where('scydgy_id', 'in', array_keys($sidList))
+                        ->field('scydgy_id,result')
+                        ->select();
+                } catch (\Throwable $e) {
+                    $inRows = [];
+                }
+                if (is_array($inRows)) {
+                    foreach ($inRows as $ir) {
+                        if (!is_array($ir)) {
+                            continue;
+                        }
+                        $isid = (int)($ir['scydgy_id'] ?? 0);
+                        if ($isid !== 0) {
+                            $inboundMap[$isid] = trim((string)($ir['result'] ?? ''));
+                        }
+                    }
+                }
+            }
 
             $out = [];
             foreach ($rows as $r) {
@@ -150,6 +172,7 @@ class Procuremenarchive extends Backend
                     'CYJMC'             => trim((string)($r['CYJMC'] ?? '')),
                     'CGYMC'             => trim((string)($r['CGYMC'] ?? '')),
                     'pick_company_name' => trim((string)($r['pick_company_name'] ?? '')),
+                    'inbound_result'    => $inboundMap[$sid] ?? '',
                     'createtime'        => $done['ts'],
                     'createtime_text'   => $done['text'],
                 ];
@@ -220,6 +243,7 @@ class Procuremenarchive extends Backend
             $head = $groupRows[0];
             $gymcList = [];
             $supplierList = [];
+            $inboundList = [];
             foreach ($groupRows as $r) {
                 $g = trim((string)($r['CGYMC'] ?? ''));
                 if ($g !== '' && !in_array($g, $gymcList, true)) {
@@ -229,12 +253,17 @@ class Procuremenarchive extends Backend
                 if ($sn !== '' && !in_array($sn, $supplierList, true)) {
                     $supplierList[] = $sn;
                 }
+                $ir = trim((string)($r['inbound_result'] ?? ''));
+                if ($ir !== '' && !in_array($ir, $inboundList, true)) {
+                    $inboundList[] = $ir;
+                }
             }
             $merged = $head;
             if (count($groupRows) > 1) {
                 $merged['CGYMC'] = implode('、', $gymcList);
             }
             $merged['pick_company_name'] = implode('、', $supplierList);
+            $merged['inbound_result'] = $inboundList !== [] ? implode('、', $inboundList) : '';
             $merged['process_count'] = count($groupRows);
             $latestTs = 0;
             $latestText = '';

+ 1 - 0
application/admin/controller/Procuremenmenu.php

@@ -208,6 +208,7 @@ class Procuremenmenu extends Backend
             if ($m['name'] === 'supplierservicescore/index') {
                 foreach ([
                     ['name' => 'supplierservicescore/export', 'title' => '导出供应商评审表'],
+                    ['name' => 'supplierservicescore/monthorders', 'title' => '本月订单明细'],
                 ] as $cr) {
                     if (Db::name('auth_rule')->where('name', $cr['name'])->find()) {
                         continue;

+ 15 - 22
application/admin/controller/Supplierscorerule.php

@@ -19,7 +19,7 @@ class Supplierscorerule extends Backend
     /** @var \app\admin\model\Supplierscorerule */
     protected $model = null;
 
-    protected $searchFields = 'id';
+    protected $searchFields = 'id,name';
 
     public function _initialize()
     {
@@ -31,13 +31,16 @@ class Supplierscorerule extends Backend
     public function index()
     {
         $this->request->filter(['strip_tags', 'trim']);
+        // 打开列表即补 name 字段并写入三条订单类型方案
+        ProcuremenSupplierScore::ensureSchema();
         if ($this->request->isAjax()) {
             list($where, $sort, $order, $offset, $limit) = $this->buildparams();
             $list = $this->model->where($where)->order($sort, $order)->paginate($limit);
             $rows = [];
             foreach ($list as $row) {
                 $arr = $row->toArray();
-                $arr['is_default_text'] = !empty($arr['is_default']) ? '使用' : '未使用';
+                $arr['name'] = trim((string)($arr['name'] ?? ''));
+                $arr['is_default_text'] = !empty($arr['is_default']) ? '启用' : '未启用';
                 $rows[] = $arr;
             }
 
@@ -142,11 +145,18 @@ class Supplierscorerule extends Backend
      */
     protected function normalizeRuleParams(array $params): array
     {
+        $name = trim((string)($params['name'] ?? ''));
+        if ($name === '') {
+            $this->error('请填写规则名称');
+        }
+        if (mb_strlen($name) > 100) {
+            $this->error('规则名称过长');
+        }
         $qw = trim((string)($params['quality_weight'] ?? ''));
         $pw = trim((string)($params['price_weight'] ?? ''));
         $lw = trim((string)($params['lead_weight'] ?? '0'));
         if ($qw === '' || !preg_match('/^\d+$/', $qw)) {
-            $this->error('商务/技术分%须为非负整数');
+            $this->error('质量分%须为非负整数');
         }
         if ($pw === '' || !preg_match('/^\d+$/', $pw)) {
             $this->error('价格分%须为非负整数');
@@ -155,7 +165,7 @@ class Supplierscorerule extends Backend
             $lw = '0';
         }
         if (!preg_match('/^\d+$/', $lw)) {
-            $this->error('交分%须为非负整数');
+            $this->error('交分%须为非负整数');
         }
         $qwNum = (int)$qw;
         $pwNum = (int)$pw;
@@ -163,30 +173,13 @@ class Supplierscorerule extends Backend
         if ($qwNum + $pwNum + $lwNum <= 0) {
             $this->error('至少一项权重大于 0');
         }
-        $status = trim((string)($params['status'] ?? 'normal'));
-        if (!in_array($status, ['normal', 'hidden'], true)) {
-            $status = 'normal';
-        }
-        $parts = [];
-        if ($qwNum > 0) {
-            $parts[] = '商务/技术分' . $qwNum . '%';
-        }
-        if ($pwNum > 0) {
-            $parts[] = '价格分' . $pwNum . '%';
-        }
-        if ($lwNum > 0) {
-            $parts[] = '交货分' . $lwNum . '%';
-        }
 
         return [
-            'name'           => $parts !== [] ? implode('+', $parts) : '规则',
+            'name'           => $name,
             'quality_weight' => $qwNum,
             'price_weight'   => $pwNum,
             'lead_weight'    => $lwNum,
-            // 兼容旧字段:权重大于 0 即视为纳入
-            'lead_enabled'   => $lwNum > 0 ? 1 : 0,
             'is_default'     => !empty($params['is_default']) ? 1 : 0,
-            'status'         => $status,
         ];
     }
 

+ 496 - 42
application/admin/controller/Supplierservicescore.php

@@ -10,7 +10,8 @@ use PhpOffice\PhpSpreadsheet\Style\Border;
 use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
 
 /**
- * 供应商评审表(月度记录:商务/技术得分 / 价格得分 / 交货得分 / 最终得分,不参与其它业务计算)
+ * 供应商评审表(月度记录:质量得分 / 价格得分 / 交货得分 / 最终得分)
+ * 订单经入库评分(合格/不合格)完结后,可在此对供应商做月度评分
  *
  * @icon fa fa-trophy
  */
@@ -21,8 +22,8 @@ class Supplierservicescore extends Backend
 
     protected $searchFields = 'company_name';
 
-    /** 有列表权限即可导出/批量保存最终得分 */
-    protected $noNeedRight = ['export', 'savefinalbatch'];
+    /** 有列表权限即可导出/批量保存最终得分/查看本月订单 */
+    protected $noNeedRight = ['export', 'savefinalbatch', 'monthorders'];
 
     public function _initialize()
     {
@@ -54,21 +55,23 @@ class Supplierservicescore extends Backend
             foreach ($pageRows as $i => $item) {
                 $rowYm = (string)($item['ym'] ?? $ym);
                 $rows[] = [
-                    'id'                   => $seqBase + $i + 1,
-                    'seq_no'               => $seqBase + $i + 1,
-                    'company_name'         => (string)($item['company_name'] ?? ''),
-                    'quality_score'        => $item['quality_score'] ?? null,
-                    'quality_score_text'   => (string)($item['quality_score_text'] ?? ''),
-                    'price_score'          => $item['price_score'] ?? null,
-                    'price_score_text'     => (string)($item['price_score_text'] ?? ''),
-                    'delivery_score'       => $item['delivery_score'] ?? null,
-                    'delivery_score_text'  => (string)($item['delivery_score_text'] ?? ''),
-                    'final_score'          => $item['final_score'] ?? null,
-                    'final_score_text'     => (string)($item['final_score_text'] ?? ''),
-                    'final_score_saved'    => !empty($item['final_score_saved']) ? 1 : 0,
-                    'score_grade'          => (string)($item['score_grade'] ?? ''),
-                    'score_date'           => (string)(($item['score_date'] ?? '') !== '' ? $item['score_date'] : $rowYm),
-                    'ym'                   => $rowYm,
+                    'id'                       => $seqBase + $i + 1,
+                    'seq_no'                   => $seqBase + $i + 1,
+                    'company_name'             => (string)($item['company_name'] ?? ''),
+                    'quality_score'            => $item['quality_score'] ?? null,
+                    'quality_score_text'       => (string)($item['quality_score_text'] ?? ''),
+                    'price_score'              => $item['price_score'] ?? null,
+                    'price_score_text'         => (string)($item['price_score_text'] ?? ''),
+                    'delivery_score'           => $item['delivery_score'] ?? null,
+                    'delivery_score_text'      => (string)($item['delivery_score_text'] ?? ''),
+                    'value_added_score'        => $item['value_added_score'] ?? null,
+                    'value_added_score_text'   => (string)($item['value_added_score_text'] ?? ''),
+                    'final_score'              => $item['final_score'] ?? null,
+                    'final_score_text'         => (string)($item['final_score_text'] ?? ''),
+                    'final_score_saved'        => !empty($item['final_score_saved']) ? 1 : 0,
+                    'score_grade'              => (string)($item['score_grade'] ?? ''),
+                    'score_date'               => (string)(($item['score_date'] ?? '') !== '' ? $item['score_date'] : $rowYm),
+                    'ym'                       => $rowYm,
                 ];
             }
 
@@ -138,6 +141,32 @@ class Supplierservicescore extends Backend
         $this->success('操作成功');
     }
 
+    /**
+     * 本月订单明细(某供应商 + 年月)
+     */
+    public function monthorders()
+    {
+        if (!$this->auth->check('supplierservicescore/index') && !$this->auth->isSuperAdmin()) {
+            $this->error(__('You have no permission'));
+        }
+        $this->request->filter(['strip_tags', 'trim']);
+        $ym = trim((string)$this->request->param('ym', ''));
+        if (!preg_match('/^\d{4}-\d{2}$/', $ym)) {
+            $ym = date('Y-m');
+        }
+        $company = trim((string)$this->request->param('company_name', ''));
+        if ($company === '') {
+            $company = trim((string)$this->request->param('company', ''));
+        }
+        $rows = $company !== '' ? $this->loadMonthOrdersForSupplier($ym, $company) : [];
+        $this->view->assign('ym', $ym);
+        $this->view->assign('company_name', $company);
+        $this->view->assign('orderRows', $rows);
+        $this->view->assign('orderCount', count($rows));
+
+        return $this->view->fetch();
+    }
+
     /**
      * 导出供应商评审表
      * 列:序号、供应商、最终得分、评分等级
@@ -278,7 +307,7 @@ class Supplierservicescore extends Backend
         try {
             $rows = \think\Db::table(ProcuremenSupplierScore::TABLE_FINAL)
                 ->where('company_name', 'like', '%' . addcslashes($keyword, '%_\\') . '%')
-                ->field('ym,company_name,score,quality_score,price_score,delivery_score,final_score,score_grade')
+                ->field('ym,company_name,score,quality_score,price_score,delivery_score,value_added_score,final_score,score_grade')
                 ->order('ym', 'desc')
                 ->order('company_name', 'asc')
                 ->select();
@@ -301,15 +330,17 @@ class Supplierservicescore extends Backend
             $quality = ProcuremenSupplierScore::readOptionalScoreValue($r['quality_score'] ?? null);
             $price = ProcuremenSupplierScore::readOptionalScoreValue($r['price_score'] ?? null);
             $delivery = ProcuremenSupplierScore::readOptionalScoreValue($r['delivery_score'] ?? null);
+            $valueAdded = ProcuremenSupplierScore::readOptionalScoreValue($r['value_added_score'] ?? null);
             $final = ProcuremenSupplierScore::readOptionalScoreValue($r['final_score'] ?? null);
             $item = [
-                'quality_score'  => $quality,
-                'price_score'    => $price,
-                'delivery_score' => $delivery,
-                'final_score'    => $final,
-                'final_saved'    => $final !== null ? 1 : 0,
-                'score_grade'    => ProcuremenSupplierScore::normalizeScoreGrade($r['score_grade'] ?? ''),
-                'score_date'     => $rowYm,
+                'quality_score'     => $quality,
+                'price_score'       => $price,
+                'delivery_score'    => $delivery,
+                'value_added_score' => $valueAdded,
+                'final_score'       => $final,
+                'final_saved'       => $final !== null ? 1 : 0,
+                'score_grade'       => ProcuremenSupplierScore::normalizeScoreGrade($r['score_grade'] ?? ''),
+                'score_date'        => $rowYm,
             ];
             $list[] = $this->buildSupplierScoreListItem($name, $item, $rowYm);
         }
@@ -327,6 +358,7 @@ class Supplierservicescore extends Backend
         $quality = array_key_exists('quality_score', $item) ? $item['quality_score'] : null;
         $price = array_key_exists('price_score', $item) ? $item['price_score'] : null;
         $delivery = array_key_exists('delivery_score', $item) ? $item['delivery_score'] : null;
+        $valueAdded = array_key_exists('value_added_score', $item) ? $item['value_added_score'] : null;
         if ($quality !== null) {
             $quality = (float)$quality;
         }
@@ -336,31 +368,43 @@ class Supplierservicescore extends Backend
         if ($delivery !== null) {
             $delivery = (float)$delivery;
         }
+        if ($valueAdded !== null) {
+            $valueAdded = (float)$valueAdded;
+        }
         $hasFinal = !empty($item['final_saved']);
         $final = $hasFinal ? (float)($item['final_score'] ?? 0) : null;
-        $sortScore = $hasFinal
+        if ($final === null) {
+            $final = ProcuremenSupplierScore::sumFinalScore($quality, $price, $delivery, $valueAdded);
+        }
+        $grade = ProcuremenSupplierScore::normalizeScoreGrade($item['score_grade'] ?? '');
+        if ($grade === '' && $final !== null) {
+            $grade = ProcuremenSupplierScore::gradeFromFinalScore($final);
+        }
+        $sortScore = $final !== null
             ? (float)$final
-            : (($quality ?? 0) + ($price ?? 0) + ($delivery ?? 0));
+            : (($quality ?? 0) + ($price ?? 0) + ($delivery ?? 0) + ($valueAdded ?? 0));
         $scoreDate = (string)($item['score_date'] ?? '');
         if ($scoreDate === '') {
             $scoreDate = ProcuremenSupplierScore::formatScoreYm($ym);
         }
 
         return [
-            'company_name'         => $name,
-            'ym'                   => ProcuremenSupplierScore::formatScoreYm($ym),
-            'quality_score'        => $quality,
-            'quality_score_text'   => ProcuremenSupplierScore::formatOptionalScore($quality),
-            'price_score'          => $price,
-            'price_score_text'     => ProcuremenSupplierScore::formatOptionalScore($price),
-            'delivery_score'       => $delivery,
-            'delivery_score_text'  => ProcuremenSupplierScore::formatOptionalScore($delivery),
-            'final_score'          => $final,
-            'final_score_text'     => $hasFinal ? ProcuremenSupplierScore::formatOptionalScore($final) : '',
-            'final_score_saved'    => $hasFinal ? 1 : 0,
-            'score_grade'          => ProcuremenSupplierScore::normalizeScoreGrade($item['score_grade'] ?? ''),
-            'score_date'           => $scoreDate,
-            'sort_score'           => $sortScore,
+            'company_name'             => $name,
+            'ym'                       => ProcuremenSupplierScore::formatScoreYm($ym),
+            'quality_score'            => $quality,
+            'quality_score_text'       => ProcuremenSupplierScore::formatOptionalScore($quality),
+            'price_score'              => $price,
+            'price_score_text'         => ProcuremenSupplierScore::formatOptionalScore($price),
+            'delivery_score'           => $delivery,
+            'delivery_score_text'      => ProcuremenSupplierScore::formatOptionalScore($delivery),
+            'value_added_score'        => $valueAdded,
+            'value_added_score_text'   => ProcuremenSupplierScore::formatOptionalScore($valueAdded),
+            'final_score'              => $final,
+            'final_score_text'         => $final !== null ? ProcuremenSupplierScore::formatOptionalScore($final) : '',
+            'final_score_saved'        => $hasFinal ? 1 : 0,
+            'score_grade'              => $grade,
+            'score_date'               => $scoreDate,
+            'sort_score'               => $sortScore,
         ];
     }
 
@@ -423,4 +467,414 @@ class Supplierservicescore extends Backend
 
         return '';
     }
+
+    /**
+     * 某供应商某月参与评分的订单明细(开标/指定后按时间写入的评分记录)
+     * 一单一行(多工序顿号间隔);按开标/指定时间倒序,不把整月汇总成一行
+     *
+     * @return array<int, array<string, mixed>>
+     */
+    /**
+     * 本月订单明细:已下发订单按「下发批次」(pick_time) 分行;
+     * 同一批次内多工序顿号合并,不同下发时间不合并。
+     *
+     * @return array<int, array<string, mixed>>
+     */
+    protected function loadMonthOrdersForSupplier(string $ym, string $companyName): array
+    {
+        $ym = trim($ym);
+        $companyName = trim($companyName);
+        if (!preg_match('/^\d{4}-\d{2}$/', $ym) || $companyName === '') {
+            return [];
+        }
+        ProcuremenSupplierScore::ensureSchema();
+        try {
+            $scoreRows = \think\Db::table(ProcuremenSupplierScore::TABLE_SCORE)
+                ->where('ym', $ym)
+                ->where('company_name', $companyName)
+                ->field('ccydh,score,rank_no,quality_score,price_score,price_sum,lead_score,lead_days_sum,createtime')
+                ->order('createtime', 'desc')
+                ->order('ccydh', 'asc')
+                ->select();
+        } catch (\Throwable $e) {
+            return [];
+        }
+        if (!is_array($scoreRows) || $scoreRows === []) {
+            return [];
+        }
+        $scoreByCcydh = [];
+        foreach ($scoreRows as $r) {
+            if (!is_array($r)) {
+                continue;
+            }
+            $c = trim((string)($r['ccydh'] ?? ''));
+            // 手工新增订单号 YW… 不进本月订单明细
+            if ($c === '' || stripos($c, 'YW') === 0) {
+                continue;
+            }
+            // 同单可能有重复评分行时保留最新一条(已按 createtime desc)
+            if (!isset($scoreByCcydh[$c])) {
+                $scoreByCcydh[$c] = $r;
+            }
+        }
+        if ($scoreByCcydh === []) {
+            return [];
+        }
+        $manualCcydh = [];
+        /** @var array<string, array<string, array<string, mixed>>> $batchesByCcydh */
+        $batchesByCcydh = [];
+        /** @var array<int, array<string, mixed>> $issuedPoRows */
+        $issuedPoRows = [];
+        $sidList = [];
+        try {
+            $poRows = \think\Db::table('purchase_order')
+                ->where('CCYDH', 'in', array_keys($scoreByCcydh))
+                ->whereRaw('(mod_rq IS NULL OR TRIM(CAST(mod_rq AS CHAR(32))) = \'\' OR TRIM(CAST(mod_rq AS CHAR(32))) LIKE \'0000-00-00%\')')
+                ->field('CCYDH,CYJMC,CGYMC,CCLBMMC,CDW,This_quantity,ceilingPrice,pick_company_name,wflow_status,status,pick_time,scydgy_id')
+                ->order('id', 'asc')
+                ->select();
+        } catch (\Throwable $e) {
+            $poRows = [];
+        }
+        if (is_array($poRows)) {
+            foreach ($poRows as $po) {
+                if (!is_array($po)) {
+                    continue;
+                }
+                $c = trim((string)($po['CCYDH'] ?? ''));
+                if ($c === '' || !isset($scoreByCcydh[$c])) {
+                    continue;
+                }
+                $sid = (int)($po['scydgy_id'] ?? 0);
+                if ($sid < 0) {
+                    $manualCcydh[$c] = true;
+                    continue;
+                }
+                if (isset($manualCcydh[$c])) {
+                    continue;
+                }
+                $pickTime = trim((string)($po['pick_time'] ?? ''));
+                if ($pickTime === '' || preg_match('/^0000-00-00/', $pickTime)) {
+                    continue;
+                }
+                $issuedPoRows[] = $po;
+                if ($sid > 0) {
+                    $sidList[$sid] = true;
+                }
+            }
+        }
+        // 按工序首次下发日志时间分批(避免整单 pick_time 被后次下发覆盖后合到一行)
+        $firstIssueBySid = $this->loadFirstIssueTimeByScydgyIds(array_keys($sidList));
+        foreach ($issuedPoRows as $po) {
+            if (isset($manualCcydh[trim((string)($po['CCYDH'] ?? ''))])) {
+                continue;
+            }
+            $c = trim((string)($po['CCYDH'] ?? ''));
+            $sid = (int)($po['scydgy_id'] ?? 0);
+            $pickTime = trim((string)($po['pick_time'] ?? ''));
+            $batchKey = '';
+            if ($sid > 0 && isset($firstIssueBySid[$sid])) {
+                $batchKey = $firstIssueBySid[$sid];
+            }
+            if ($batchKey === '') {
+                $batchKey = $pickTime;
+            }
+            if ($batchKey === '' || preg_match('/^0000-00-00/', $batchKey)) {
+                continue;
+            }
+            // 同一分钟内视为同一次下发(循环写入可能差 1 秒)
+            $batchGroup = $batchKey;
+            if (preg_match('/^(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2})/', $batchKey, $m)) {
+                $batchGroup = $m[1];
+            }
+            if (!isset($batchesByCcydh[$c])) {
+                $batchesByCcydh[$c] = [];
+            }
+            if (!isset($batchesByCcydh[$c][$batchGroup])) {
+                $batchesByCcydh[$c][$batchGroup] = [
+                    'scydgy_id'         => $sid > 0 ? $sid : 0,
+                    'scydgy_ids'        => [],
+                    'CYJMC'             => trim((string)($po['CYJMC'] ?? '')),
+                    'CCLBMMC'           => trim((string)($po['CCLBMMC'] ?? '')),
+                    'CGYMC_list'        => [],
+                    'CDW'               => trim((string)($po['CDW'] ?? '')),
+                    'pick_company_name' => trim((string)($po['pick_company_name'] ?? '')),
+                    'wflow_status'      => trim((string)($po['wflow_status'] ?? '')),
+                    'status'            => trim((string)($po['status'] ?? '')),
+                    'pick_time'         => $batchKey,
+                    'completed'         => false,
+                    'picked_match'      => false,
+                ];
+            }
+            $batch = &$batchesByCcydh[$c][$batchGroup];
+            if ($batch['scydgy_id'] <= 0 && $sid > 0) {
+                $batch['scydgy_id'] = $sid;
+            }
+            if ($sid > 0 && !in_array($sid, $batch['scydgy_ids'], true)) {
+                $batch['scydgy_ids'][] = $sid;
+            }
+            // 展示时间取该批次内最早时间
+            if ($batch['pick_time'] === '' || strcmp($batchKey, $batch['pick_time']) < 0) {
+                $batch['pick_time'] = $batchKey;
+            }
+            if ($batch['CYJMC'] === '') {
+                $batch['CYJMC'] = trim((string)($po['CYJMC'] ?? ''));
+            }
+            if ($batch['CCLBMMC'] === '') {
+                $batch['CCLBMMC'] = trim((string)($po['CCLBMMC'] ?? ''));
+            }
+            $g = trim((string)($po['CGYMC'] ?? ''));
+            if ($g !== '' && !in_array($g, $batch['CGYMC_list'], true)) {
+                $batch['CGYMC_list'][] = $g;
+            }
+            $pn = trim((string)($po['pick_company_name'] ?? ''));
+            if ($pn !== '') {
+                if ($batch['pick_company_name'] === '') {
+                    $batch['pick_company_name'] = $pn;
+                }
+                if ($pn === $companyName) {
+                    $batch['picked_match'] = true;
+                }
+            }
+            if (\app\common\library\ProcuremenStatus::isPoCompleted($po['status'] ?? '')) {
+                $batch['completed'] = true;
+                $batch['status'] = \app\common\library\ProcuremenStatus::PO_COMPLETED;
+            }
+            if (\app\common\library\ProcuremenStatus::isWflowApproved($po['wflow_status'] ?? '')) {
+                $batch['wflow_status'] = \app\common\library\ProcuremenStatus::WFLOW_APPROVED;
+            } elseif ($batch['wflow_status'] === '' && trim((string)($po['wflow_status'] ?? '')) !== '') {
+                $batch['wflow_status'] = trim((string)$po['wflow_status']);
+            }
+            unset($batch);
+        }
+
+        $out = [];
+        $allSids = [];
+        foreach ($batchesByCcydh as $ccydhBatches) {
+            if (!is_array($ccydhBatches)) {
+                continue;
+            }
+            foreach ($ccydhBatches as $batch) {
+                if (!is_array($batch)) {
+                    continue;
+                }
+                foreach ((array)($batch['scydgy_ids'] ?? []) as $sidItem) {
+                    $sidItem = (int)$sidItem;
+                    if ($sidItem > 0) {
+                        $allSids[$sidItem] = true;
+                    }
+                }
+                $one = (int)($batch['scydgy_id'] ?? 0);
+                if ($one > 0) {
+                    $allSids[$one] = true;
+                }
+            }
+        }
+        $inboundBySid = $this->loadInboundResultByScydgyIds(array_keys($allSids));
+
+        foreach ($scoreByCcydh as $ccydh => $r) {
+            if (isset($manualCcydh[$ccydh]) || !isset($batchesByCcydh[$ccydh])) {
+                continue;
+            }
+            foreach ($batchesByCcydh[$ccydh] as $batch) {
+                if (!is_array($batch)) {
+                    continue;
+                }
+                $pickTimeRaw = trim((string)($batch['pick_time'] ?? ''));
+                if ($pickTimeRaw === '') {
+                    continue;
+                }
+                $gymcList = is_array($batch['CGYMC_list'] ?? null) ? $batch['CGYMC_list'] : [];
+                // 与详情页一致:采购确认通过或已完结后才展示中标/未中标
+                $showBid = !empty($batch['completed'])
+                    || \app\common\library\ProcuremenStatus::isPoCompleted($batch['status'] ?? '')
+                    || \app\common\library\ProcuremenStatus::isWflowApproved($batch['wflow_status'] ?? '');
+                $isPicked = !empty($batch['picked_match']) ? 1 : 0;
+                $inboundResult = '';
+                $sidCandidates = is_array($batch['scydgy_ids'] ?? null) ? $batch['scydgy_ids'] : [];
+                $mainSid = (int)($batch['scydgy_id'] ?? 0);
+                if ($mainSid > 0 && !in_array($mainSid, $sidCandidates, true)) {
+                    $sidCandidates[] = $mainSid;
+                }
+                foreach ($sidCandidates as $sidItem) {
+                    $sidItem = (int)$sidItem;
+                    if ($sidItem > 0 && isset($inboundBySid[$sidItem]) && $inboundBySid[$sidItem] !== '') {
+                        $inboundResult = $inboundBySid[$sidItem];
+                        break;
+                    }
+                }
+                $out[] = [
+                    'seq_no'            => 0,
+                    'scydgy_id'         => (int)($batch['scydgy_id'] ?? 0),
+                    'CCYDH'             => $ccydh,
+                    'CYJMC'             => (string)($batch['CYJMC'] ?? ''),
+                    'CCLBMMC'           => (string)($batch['CCLBMMC'] ?? ''),
+                    'CGYMC'             => $gymcList !== [] ? implode('、', $gymcList) : '',
+                    'CDW'               => (string)($batch['CDW'] ?? ''),
+                    'pick_company_name' => (string)($batch['pick_company_name'] ?? ''),
+                    'wflow_status'      => (string)($batch['wflow_status'] ?? ''),
+                    'status'            => (string)($batch['status'] ?? ''),
+                    'progress_text'     => $this->formatMonthOrderProgressText($batch),
+                    'pick_time'         => $pickTimeRaw,
+                    'issue_time'        => \app\common\library\ProcuremenTime::formatDisplayDateTime($pickTimeRaw),
+                    'score'             => $r['score'] ?? null,
+                    'rank_no'           => (int)($r['rank_no'] ?? 0),
+                    'quality_score'     => $r['quality_score'] ?? null,
+                    'price_score'       => $r['price_score'] ?? null,
+                    'price_sum'         => $r['price_sum'] ?? null,
+                    'lead_score'        => $r['lead_score'] ?? null,
+                    'lead_days_sum'     => $r['lead_days_sum'] ?? null,
+                    'is_picked'         => $isPicked,
+                    'show_bid_result'   => $showBid ? 1 : 0,
+                    'inbound_result'    => $inboundResult,
+                ];
+            }
+        }
+
+        usort($out, static function ($a, $b) {
+            $ta = (string)($a['pick_time'] ?? '');
+            $tb = (string)($b['pick_time'] ?? '');
+            if ($ta !== $tb) {
+                return strcmp($tb, $ta);
+            }
+            $ca = (string)($a['CCYDH'] ?? '');
+            $cb = (string)($b['CCYDH'] ?? '');
+
+            return strcmp($ca, $cb);
+        });
+        $total = count($out);
+        $seq = $total;
+        foreach ($out as &$row) {
+            $row['seq_no'] = $seq;
+            $seq--;
+        }
+        unset($row);
+
+        return $out;
+    }
+
+    /**
+     * 批量取各工序首次「下发」操作时间(用于本月明细按批次拆行)
+     *
+     * @param int[] $scydgyIds
+     * @return array<int, string> scydgy_id => Y-m-d H:i:s
+     */
+    protected function loadFirstIssueTimeByScydgyIds(array $scydgyIds): array
+    {
+        $scydgyIds = array_values(array_unique(array_filter(array_map('intval', $scydgyIds), static function ($id) {
+            return $id > 0;
+        })));
+        if ($scydgyIds === []) {
+            return [];
+        }
+        $acts = \app\common\library\ProcuremenOperLog::expandActionQueryValues('issue_submit');
+        if ($acts === []) {
+            return [];
+        }
+        try {
+            $logs = \think\Db::table('purchase_order_oper_log')
+                ->where(\app\common\library\ProcuremenOperLog::COL_SCYDGY_ID, 'in', $scydgyIds)
+                ->where(\app\common\library\ProcuremenOperLog::COL_ACTION, 'in', $acts)
+                ->field(\app\common\library\ProcuremenOperLog::COL_SCYDGY_ID . ',' . \app\common\library\ProcuremenOperLog::COL_TIME)
+                ->order('id', 'asc')
+                ->select();
+        } catch (\Throwable $e) {
+            return [];
+        }
+        $map = [];
+        if (!is_array($logs)) {
+            return [];
+        }
+        foreach ($logs as $lg) {
+            if (!is_array($lg)) {
+                continue;
+            }
+            $sid = (int)($lg[\app\common\library\ProcuremenOperLog::COL_SCYDGY_ID] ?? 0);
+            if ($sid <= 0 || isset($map[$sid])) {
+                continue;
+            }
+            $t = trim((string)($lg[\app\common\library\ProcuremenOperLog::COL_TIME] ?? ''));
+            if ($t === '' || preg_match('/^0000-00-00/', $t)) {
+                continue;
+            }
+            if (is_numeric($t) && (int)$t > 946684800) {
+                $t = date('Y-m-d H:i:s', (int)$t);
+            } else {
+                $t = \app\common\library\ProcuremenTime::formatDisplayDateTime($t);
+            }
+            if ($t === '') {
+                continue;
+            }
+            $map[$sid] = $t;
+        }
+
+        return $map;
+    }
+
+    /**
+     * 当前进度:已完结优先;终审通过也显示已完结;其余展示流程阶段
+     *
+     * @param array<string, mixed> $po
+     */
+    protected function formatMonthOrderProgressText(array $po): string
+    {
+        if (!empty($po['completed']) || \app\common\library\ProcuremenStatus::isPoCompleted($po['status'] ?? '')) {
+            return \app\common\library\ProcuremenStatus::PO_COMPLETED;
+        }
+        // 采购终审通过后待入库评分,合格/不合格后才完结
+        if (\app\common\library\ProcuremenStatus::isWflowApproved($po['wflow_status'] ?? '')) {
+            return '待入库评分';
+        }
+        $w = \app\common\library\ProcuremenStatus::normalizeWflowStatus($po['wflow_status'] ?? '');
+        if ($w === \app\common\library\ProcuremenStatus::WFLOW_PENDING_APPROVAL) {
+            return '待采购确认';
+        }
+        if ($w === \app\common\library\ProcuremenStatus::WFLOW_PENDING_CONFIRM) {
+            return '待确认供应商';
+        }
+        if ($w === \app\common\library\ProcuremenStatus::WFLOW_PENDING_ISSUE) {
+            return '待协助下发';
+        }
+
+        return $w !== '' ? $w : '—';
+    }
+
+    /**
+     * 批量读取入库评分结果(合格/不合格)
+     *
+     * @param int[] $scydgyIds
+     * @return array<int, string> scydgy_id => 合格|不合格
+     */
+    protected function loadInboundResultByScydgyIds(array $scydgyIds): array
+    {
+        $ids = array_values(array_unique(array_filter(array_map('intval', $scydgyIds))));
+        if ($ids === []) {
+            return [];
+        }
+        $out = [];
+        try {
+            $rows = \think\Db::table('purchase_order_inbound_score')
+                ->where('scydgy_id', 'in', $ids)
+                ->field('scydgy_id,result')
+                ->select();
+        } catch (\Throwable $e) {
+            return [];
+        }
+        if (!is_array($rows)) {
+            return [];
+        }
+        foreach ($rows as $r) {
+            if (!is_array($r)) {
+                continue;
+            }
+            $sid = (int)($r['scydgy_id'] ?? 0);
+            $result = trim((string)($r['result'] ?? ''));
+            if ($sid <= 0 || ($result !== '合格' && $result !== '不合格')) {
+                continue;
+            }
+            $out[$sid] = $result;
+        }
+
+        return $out;
+    }
 }

+ 14 - 9
application/admin/model/Purchasesmstemplate.php

@@ -18,11 +18,12 @@ class Purchasesmstemplate extends Model
     public function getSceneList()
     {
         return [
-            'review_email' => '协助下发-邮箱',
-            'review_sms'   => '协助下发-短信',
-            'confirm_ok'   => '采购确认-通过',
-            'confirm_fail' => '采购确认-未通过',
-            'bid_open'     => '开标双重验证',
+            'review_email'       => '协助下发-邮箱',
+            'review_sms'         => '协助下发-短信',
+            'confirm_ok'         => '采购确认-通过',
+            'confirm_fail'       => '采购确认-未通过',
+            'bid_open'           => '开标双重验证',
+            'rfq_salesman_email' => '询价通知业务员-邮箱',
         ];
     }
 
@@ -34,7 +35,7 @@ class Purchasesmstemplate extends Model
 
     public static function isEmailScene(string $scene): bool
     {
-        return $scene === 'review_email' || $scene === 'review';
+        return $scene === 'review_email' || $scene === 'review' || $scene === 'rfq_salesman_email';
     }
 
     public static function isSmsScene(string $scene): bool
@@ -63,12 +64,16 @@ class Purchasesmstemplate extends Model
     {
         return [
             ['tag' => '{company_name}', 'label' => '供应商名称', 'example' => '浙江某某印刷有限公司', 'scenes' => '全部'],
-            ['tag' => '{contact_name}', 'label' => '姓名', 'example' => '张三', 'scenes' => '全部'],
+            ['tag' => '{contact_name}', 'label' => '姓名/业务员', 'example' => '张三', 'scenes' => '全部'],
+            ['tag' => '{salesman_name}', 'label' => '业务员姓名', 'example' => '张三', 'scenes' => '询价通知业务员'],
             ['tag' => '{phone}', 'label' => '手机号', 'example' => '13800138000', 'scenes' => '全部'],
             ['tag' => '{email}', 'label' => '邮箱', 'example' => 'user@example.com', 'scenes' => '全部'],
-            ['tag' => '{ccydh}', 'label' => '订单号', 'example' => '202603668L', 'scenes' => '全部'],
+            ['tag' => '{ccydh}', 'label' => '订单号/需求编号', 'example' => 'YW20260730001', 'scenes' => '全部'],
             ['tag' => '{cyjmc}', 'label' => '印件名称', 'example' => '藏书票2', 'scenes' => '全部'],
-            ['tag' => '{cgymc}', 'label' => '工序名称(单道工序)', 'example' => '骑马订', 'scenes' => '协助下发'],
+            ['tag' => '{cgymc}', 'label' => '工序名称(单道工序)', 'example' => '骑马订', 'scenes' => '协助下发/询价通知'],
+            ['tag' => '{this_quantity}', 'label' => '本次数量', 'example' => '200', 'scenes' => '询价通知业务员'],
+            ['tag' => '{supplier_name}', 'label' => '供应商', 'example' => '台州某某有限公司', 'scenes' => '询价通知业务员'],
+            ['tag' => '{cywyxm}', 'label' => '需求发起人', 'example' => '管理员', 'scenes' => '询价通知业务员'],
             ['tag' => '{category}', 'label' => '业务分类', 'example' => '出版物印刷', 'scenes' => '协助下发'],
             ['tag' => '{deadline}', 'label' => '截止时间', 'example' => '2026-05-18 14:30', 'scenes' => '协助下发'],
             ['tag' => '{process_lines}', 'label' => '订单工序明细(文本)', 'example' => "订单号:YW20240629001\n印件名称:藏书票2\n1.工序名称:做刀版 单位:张 本次数量:500", 'scenes' => '全部'],

+ 45 - 36
application/admin/view/dashboard/index.html

@@ -208,16 +208,28 @@
         grid-template-columns: repeat(3, minmax(0, 1fr));
     }
 
+    .proc-dash-overview.cols-5 {
+        grid-template-columns: repeat(5, minmax(0, 1fr));
+    }
+
+    @media (max-width: 1199px) {
+        .proc-dash-overview.cols-5 {
+            grid-template-columns: repeat(3, minmax(0, 1fr));
+        }
+    }
+
     @media (max-width: 991px) {
         .proc-dash-overview,
-        .proc-dash-overview.cols-3 {
+        .proc-dash-overview.cols-3,
+        .proc-dash-overview.cols-5 {
             grid-template-columns: repeat(2, minmax(0, 1fr));
         }
     }
 
     @media (max-width: 479px) {
         .proc-dash-overview,
-        .proc-dash-overview.cols-3 {
+        .proc-dash-overview.cols-3,
+        .proc-dash-overview.cols-5 {
             grid-template-columns: 1fr;
         }
     }
@@ -284,6 +296,7 @@
     .proc-overview-item.item-confirm .overview-num { color: #f0ad4e; }
     .proc-overview-item.item-approval .overview-num { color: #7266ba; }
     .proc-overview-item.item-completed .overview-num { color: #27C24C; }
+    .proc-overview-item .overview-num.overview-num-fail { color: #d9534f; }
 
     .proc-overview-item.item-confirm .overview-num-single { color: #f0ad4e; }
     .proc-overview-item.item-approval .overview-num-single { color: #7266ba; }
@@ -689,64 +702,60 @@
 </style>
 <?php $ps = isset($procuremenStats) && is_array($procuremenStats) ? $procuremenStats : ['kpi'=>[], 'today'=>[], 'week'=>[], 'pending'=>[], 'supplierRank'=>['quote'=>[], 'selected'=>[], 'period_label'=>'本周'], 'updated'=>'']; ?>
 <?php $supplierRank = isset($ps['supplierRank']) && is_array($ps['supplierRank']) ? $ps['supplierRank'] : ['quote'=>[], 'selected'=>[], 'period_label'=>'本月', 'supplier_total'=>0]; ?>
-<?php $kpi = isset($ps['kpi']) && is_array($ps['kpi']) ? $ps['kpi'] : ['supplier'=>['total'=>0],'confirm'=>['today'=>0,'month'=>0],'approval'=>['today'=>0,'month'=>0],'completed'=>['today'=>0,'month'=>0]]; ?>
+<?php $kpi = isset($ps['kpi']) && is_array($ps['kpi']) ? $ps['kpi'] : []; ?>
 <div class="panel panel-default panel-intro">
     <div class="panel-body">
         <div class="proc-dash">
                     <div class="proc-kpi-section">
                         <div class="proc-kpi-section-title"><i class="fa fa-calendar-o"></i> 今日数据</div>
-                        <div class="proc-dash-overview cols-3">
+                        <div class="proc-dash-overview cols-5">
                             <div class="proc-overview-item item-confirm">
-                                <div class="overview-icon"><i class="fa fa-check-square-o"></i></div>
+                                <div class="overview-icon"><i class="fa fa-download"></i></div>
                                 <div class="overview-body">
-                                    <div class="overview-title">待确认</div>
-                                    <div class="overview-num overview-num-single">{$kpi.confirm.today|default='0'}</div>
-                                    <a href="javascript:;" class="overview-link addtabsit" data-url="procuremen/audit"><i class="fa fa-external-link"></i> 供应商确认</a>
+                                    <div class="overview-title">导入需求</div>
+                                    <div class="overview-num overview-num-single"><?= (int)($kpi['import']['today'] ?? 0) ?></div>
+                                    <a href="javascript:;" class="overview-link addtabsit" data-url="procuremen/pick"><i class="fa fa-external-link"></i> 采购供应商初选</a>
                                 </div>
                             </div>
                             <div class="proc-overview-item item-approval">
-                                <div class="overview-icon"><i class="fa fa-shopping-cart"></i></div>
+                                <div class="overview-icon"><i class="fa fa-check-square-o"></i></div>
                                 <div class="overview-body">
-                                    <div class="overview-title">待审批</div>
-                                    <div class="overview-num overview-num-single">{$kpi.approval.today|default='0'}</div>
-                                    <a href="javascript:;" class="overview-link addtabsit" data-url="procuremen/confirm"><i class="fa fa-external-link"></i> 供应商审批</a>
+                                    <div class="overview-title">报价供应商待确认</div>
+                                    <div class="overview-num overview-num-single"><?= (int)($kpi['quote_pending']['today'] ?? 0) ?></div>
+                                    <a href="javascript:;" class="overview-link addtabsit" data-url="procuremen/audit"><i class="fa fa-external-link"></i> 供应商确认</a>
                                 </div>
                             </div>
                             <div class="proc-overview-item item-completed">
-                                <div class="overview-icon"><i class="fa fa-flag-checkered"></i></div>
+                                <div class="overview-icon"><i class="fa fa-pencil-square-o"></i></div>
                                 <div class="overview-body">
-                                    <div class="overview-title">已完结</div>
-                                    <div class="overview-num overview-num-single">{$kpi.completed.today|default='0'}</div>
+                                    <div class="overview-title">已入库打分</div>
+                                    <div class="overview-metrics">
+                                        <div class="overview-metric">
+                                            <span class="overview-metric-label">合格</span>
+                                            <span class="overview-num"><?= (int)($kpi['inbound']['today']['pass'] ?? 0) ?></span>
+                                        </div>
+                                        <div class="overview-metric">
+                                            <span class="overview-metric-label">不合格</span>
+                                            <span class="overview-num overview-num-fail"><?= (int)($kpi['inbound']['today']['fail'] ?? 0) ?></span>
+                                        </div>
+                                    </div>
                                     <a href="javascript:;" class="overview-link addtabsit" data-url="procuremenarchive/index"><i class="fa fa-external-link"></i> 历史存证档案查询</a>
                                 </div>
                             </div>
-                        </div>
-                    </div>
-                    <div class="proc-kpi-section">
-                        <div class="proc-kpi-section-title"><i class="fa fa-calendar"></i> 本月数据</div>
-                        <div class="proc-dash-overview cols-3">
                             <div class="proc-overview-item item-confirm">
-                                <div class="overview-icon"><i class="fa fa-check-square-o"></i></div>
+                                <div class="overview-icon"><i class="fa fa-plus-square"></i></div>
                                 <div class="overview-body">
-                                    <div class="overview-title">待确认</div>
-                                    <div class="overview-num overview-num-single">{$kpi.confirm.month|default='0'}</div>
-                                    <a href="javascript:;" class="overview-link addtabsit" data-url="procuremen/audit"><i class="fa fa-external-link"></i> 供应商确认</a>
+                                    <div class="overview-title">新增询价</div>
+                                    <div class="overview-num overview-num-single"><?= (int)($kpi['rfq_new']['today'] ?? 0) ?></div>
+                                    <a href="javascript:;" class="overview-link addtabsit" data-url="procuremen/rfqlist"><i class="fa fa-external-link"></i> 查询询价</a>
                                 </div>
                             </div>
                             <div class="proc-overview-item item-approval">
-                                <div class="overview-icon"><i class="fa fa-shopping-cart"></i></div>
+                                <div class="overview-icon"><i class="fa fa-comments-o"></i></div>
                                 <div class="overview-body">
-                                    <div class="overview-title">待审批</div>
-                                    <div class="overview-num overview-num-single">{$kpi.approval.month|default='0'}</div>
-                                    <a href="javascript:;" class="overview-link addtabsit" data-url="procuremen/confirm"><i class="fa fa-external-link"></i> 供应商审批</a>
-                                </div>
-                            </div>
-                            <div class="proc-overview-item item-completed">
-                                <div class="overview-icon"><i class="fa fa-flag-checkered"></i></div>
-                                <div class="overview-body">
-                                    <div class="overview-title">已完结</div>
-                                    <div class="overview-num overview-num-single">{$kpi.completed.month|default='0'}</div>
-                                    <a href="javascript:;" class="overview-link addtabsit" data-url="procuremenarchive/index"><i class="fa fa-external-link"></i> 历史存证档案查询</a>
+                                    <div class="overview-title">供应商已报价询价</div>
+                                    <div class="overview-num overview-num-single"><?= (int)($kpi['rfq_quoted']['today'] ?? 0) ?></div>
+                                    <a href="javascript:;" class="overview-link addtabsit" data-url="procuremen/rfqlist"><i class="fa fa-external-link"></i> 查询询价</a>
                                 </div>
                             </div>
                         </div>

+ 102 - 6
application/admin/view/procuremen/add.html

@@ -13,12 +13,81 @@
     .procuremen-add-form .layer-footer .btn + .btn {
         margin-left: 12px;
     }
+    /* 多选已选项换行完整展示,避免单行省略号截断 */
+    .procuremen-add-form .bootstrap-select {
+        width: 100% !important;
+    }
+    .procuremen-add-form .bootstrap-select > .dropdown-toggle {
+        height: auto !important;
+        min-height: 34px;
+        white-space: normal;
+        text-align: left;
+        padding: 6px 28px 6px 12px;
+        line-height: 1.45;
+    }
+    .procuremen-add-form .bootstrap-select .filter-option,
+    .procuremen-add-form .bootstrap-select .filter-option-inner,
+    .procuremen-add-form .bootstrap-select .filter-option-inner-inner {
+        white-space: normal !important;
+        height: auto !important;
+        max-height: none !important;
+        overflow: visible !important;
+        text-overflow: clip !important;
+        word-break: break-all;
+        line-height: 1.45;
+    }
+    /* 统一向下展开,避免「通知业务员」因靠近底部自动向上弹 */
+    .procuremen-add-form .bootstrap-select.dropup .dropdown-menu {
+        top: 100% !important;
+        bottom: auto !important;
+        margin-top: 2px !important;
+        margin-bottom: 0 !important;
+    }
+    .procuremen-add-form .bootstrap-select.dropup .dropdown-toggle::after,
+    .procuremen-add-form .bootstrap-select.dropup .caret {
+        border-top: 4px dashed;
+        border-bottom: 0;
+    }
+    /* 需求部门:可下拉选全部,也可手输;点输入框即可展开 */
+    .procuremen-add-form .rfq-dept-combo {
+        width: 100%;
+        position: relative;
+    }
+    .procuremen-add-form .rfq-dept-combo #rfq-dept-input {
+        cursor: text;
+    }
+    .procuremen-add-form .rfq-dept-combo .dropdown-menu {
+        left: 0 !important;
+        right: 0 !important;
+        width: 100%;
+        min-width: 100%;
+        max-height: 260px;
+        overflow-y: auto;
+        margin-top: 2px;
+    }
+    .procuremen-add-form .rfq-dept-combo .input-group-btn {
+        position: static;
+    }
+    .procuremen-add-form .rfq-dept-combo .dropdown-menu > li > a {
+        padding: 6px 12px;
+        white-space: normal;
+        word-break: break-all;
+    }
+    .procuremen-add-form .rfq-dept-combo .dropdown-menu > li.rfq-dept-empty {
+        display: none;
+        padding: 8px 12px;
+        color: #999;
+        font-size: 12px;
+    }
+    .procuremen-add-form textarea[name="row[MBZ]"] {
+        resize: none;
+    }
 </style>
 
 <div class="procuremen-add-wrap">
     <form id="add-form" class="form-horizontal procuremen-add-form" role="form" data-toggle="validator" method="POST" action="">
         <div class="form-group">
-            <label class="control-label col-xs-12 col-sm-2">订单号:</label>
+            <label class="control-label col-xs-12 col-sm-2">需求编号:</label>
             <div class="col-xs-12 col-sm-8">
                 <input id="add-next-ccydh" class="form-control" type="text" value="{$nextOrderCcydh|default=''|htmlentities}" readonly>
             </div>
@@ -30,9 +99,22 @@
             </div>
         </div>
         <div class="form-group">
-            <label class="control-label col-xs-12 col-sm-2">承揽部门:</label>
+            <label class="control-label col-xs-12 col-sm-2">需求部门:</label>
             <div class="col-xs-12 col-sm-8">
-                <input class="form-control" name="row[CCLBMMC]" type="text" value="{$defaultCclbmmc|default='营销中心'|htmlentities}" autocomplete="off">
+                <div class="input-group rfq-dept-combo">
+                    <input id="rfq-dept-input" class="form-control" name="row[CCLBMMC]" type="text" value="{$defaultCclbmmc|default='营销中心'|htmlentities}" autocomplete="off" placeholder="可下拉选择或自行输入">
+                    <div class="input-group-btn">
+                        <button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" title="选择部门">
+                            <span class="caret"></span>
+                        </button>
+                        <ul class="dropdown-menu">
+                            {volist name="rfqDeptOptions" id="deptName"}
+                            <li><a href="javascript:;" class="rfq-dept-pick" data-value="{$deptName|htmlentities}">{$deptName|htmlentities}</a></li>
+                            {/volist}
+                            <li class="rfq-dept-empty">无匹配部门,可直接输入使用</li>
+                        </ul>
+                    </div>
+                </div>
             </div>
         </div>
         <div class="form-group">
@@ -66,17 +148,31 @@
             </div>
         </div>
         <div class="form-group">
-            <label class="control-label col-xs-12 col-sm-2">外厂单位:</label>
+            <label class="control-label col-xs-12 col-sm-2">选择供应商:</label>
             <div class="col-xs-12 col-sm-8">
-                <input class="form-control" name="row[cGzzxMc]" type="text" autocomplete="off">
+                <select class="form-control selectpicker" name="row[cGzzxMc][]" multiple data-live-search="true" data-dropup-auto="false" data-width="100%" data-selected-text-format="values" title="请选择供应商">
+                    {volist name="rfqSupplierOptions" id="supName"}
+                    <option value="{$supName|htmlentities}">{$supName|htmlentities}</option>
+                    {/volist}
+                </select>
             </div>
         </div>
         <div class="form-group">
-            <label class="control-label col-xs-12 col-sm-2">业务员:</label>
+            <label class="control-label col-xs-12 col-sm-2">需求发起人:</label>
             <div class="col-xs-12 col-sm-8">
                 <input class="form-control" name="row[cywyxm]" type="text" value="{$adminNickname|default=''|htmlentities}" readonly>
             </div>
         </div>
+        <div class="form-group">
+            <label class="control-label col-xs-12 col-sm-2">通知业务员:</label>
+            <div class="col-xs-12 col-sm-8">
+                <select class="form-control selectpicker" name="row[notify_salesman][]" multiple data-live-search="true" data-dropup-auto="false" data-width="100%" data-selected-text-format="values" title="请选择业务员">
+                    {volist name="rfqSalesmanOptions" id="smName"}
+                    <option value="{$smName|htmlentities}">{$smName|htmlentities}</option>
+                    {/volist}
+                </select>
+            </div>
+        </div>
         <div class="form-group">
             <label class="control-label col-xs-12 col-sm-2">备注:</label>
             <div class="col-xs-12 col-sm-8">

+ 97 - 17
application/admin/view/procuremen/audit_issue.html

@@ -33,6 +33,34 @@
     .audit-issue-wrap .audit-score-rule-tip strong {
         color: #245269;
     }
+    .audit-issue-wrap .audit-manual-pick-inline {
+        flex: 1 1 220px;
+        min-width: 0;
+        margin-left: 4px;
+        font-size: 13px;
+        font-weight: 400;
+        color: #555;
+        line-height: 1.5;
+        white-space: normal;
+        vertical-align: middle;
+    }
+    .audit-issue-wrap .audit-manual-pick-inline .audit-manual-pick-inline-sep {
+        margin: 0 8px;
+        color: #ccc;
+    }
+    .audit-issue-wrap .audit-manual-pick-inline strong {
+        color: #333;
+        font-weight: 600;
+    }
+    .audit-issue-wrap .audit-order-type-text {
+        display: inline-block;
+        min-height: 30px;
+        line-height: 30px;
+        font-size: 13px;
+        font-weight: 600;
+        color: #333;
+        vertical-align: middle;
+    }
     .audit-issue-wrap .audit-table-wrap {
         /* 不在表格区域内部滚动,由弹窗页面整体滚动 */
         max-height: none;
@@ -121,6 +149,7 @@
         position: relative;
         z-index: 61;
         overflow: visible;
+        width: 100%;
     }
     .audit-issue-wrap .audit-deadline-item {
         display: flex;
@@ -206,6 +235,14 @@
     .audit-issue-wrap .audit-deadline-item.audit-bid-open-item {
         align-items: center;
         margin-top: 0;
+        flex: 1 1 320px;
+        min-width: 0;
+    }
+    .audit-issue-wrap .audit-deadline-item.audit-manual-pick-item {
+        align-items: center;
+        margin-top: 0;
+        margin-left: auto;
+        flex-shrink: 0;
     }
     .audit-issue-wrap .audit-deadline-row .audit-append-supplier-btn {
         flex-shrink: 0;
@@ -219,6 +256,22 @@
         line-height: 20px;
         margin-left: 0;
     }
+    .audit-issue-wrap .audit-deadline-row .audit-manual-pick-btn {
+        flex-shrink: 0;
+        height: 32px;
+        line-height: 20px;
+        margin-left: 0;
+    }
+    .audit-issue-wrap .audit-deadline-row .audit-manual-picked-tag {
+        flex-shrink: 0;
+        height: 32px;
+        line-height: 32px;
+        font-size: 14px;
+        font-weight: 600;
+        color: #3c763d;
+        white-space: nowrap;
+        margin-right: 8px;
+    }
     .audit-issue-wrap .audit-deadline-row .audit-bid-open-verified-tag {
         flex-shrink: 0;
         height: 32px;
@@ -287,7 +340,6 @@
                 <col style="width:80px"/>
                 <col style="width:72px"/>
                 <col style="width:72px"/>
-                <col style="width:72px"/>
                 <col style="width:80px"/>
                 <col/>
             </colgroup>
@@ -297,7 +349,6 @@
                 <th>印件名称</th>
                 <th>工序名称</th>
                 <th class="text-center">单位</th>
-                <th class="text-center">工作量</th>
                 <th class="text-center">本次数量</th>
                 <th class="text-center">最高限价</th>
                 <th class="text-center">订法</th>
@@ -313,7 +364,6 @@
                 {/if}
                 <td class="audit-process-name">{$pr.CGYMC|default=''}</td>
                 <td class="text-center">{$pr.CDW|default=''}</td>
-                <td class="text-center">{$pr.NGZL|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>
@@ -328,6 +378,9 @@
         <input type="hidden" name="scydgy_id" value="{$scydgyId|htmlentities}"/>
         <input type="hidden" id="audit-quote-groups-json" value="{$quoteGroupsJson|htmlentities}"/>
         <input type="hidden" id="audit-bid-open-verified" value="{$bidOpenVerified|default=0}"/>
+        <input type="hidden" id="audit-manual-picked" value="{$manualPicked|default=0}"/>
+        <input type="hidden" id="audit-quote-visible" value="{$quoteVisible|default=0}"/>
+        <input type="hidden" id="audit-manual-pick-reason" value="{$manualPickReason|default=''|htmlentities}"/>
         <input type="hidden" id="audit-append-deadline-passed" value="{$appendSupplierDeadlinePassed|default=0}"/>
         <input type="hidden" id="audit-order-ccydh" value="{$orderCcydh|default=''|htmlentities}"/>
         <div class="audit-deadline-row">
@@ -349,8 +402,14 @@
                 <span class="audit-field-label">交货截止日期:</span>
                 <input type="text" class="form-control audit-delivery-deadline-input" value="{$deliveryDeadline|default=''|htmlentities}" title="交货截止日期" readonly="readonly" disabled="disabled" tabindex="-1" placeholder=""/>
             </div>
+            {if !empty($orderScoreRuleName)}
+            <div class="audit-deadline-item">
+                <span class="audit-field-label">订单类型:</span>
+                <span class="audit-order-type-text" title="下发时选定的评分规则">{$orderScoreRuleName|htmlentities}</span>
+            </div>
+            {/if}
             <div class="audit-deadline-item audit-bid-open-item">
-                {if $canAppendSupplier}
+                {if $canAppendSupplier && empty($manualPicked)}
                 {if $auth->check('procuremen/auditappend')}
                 <button type="button" class="btn btn-primary btn-sm audit-append-supplier-btn" id="btn-audit-append-supplier" title="补加供应商">
                     <i class="fa fa-plus"></i> 补加供应商
@@ -359,6 +418,17 @@
                 {/if}
                 {if $bidOpenVerified}
                 <span class="audit-bid-open-verified-tag"><i class="fa fa-check-circle"></i> 已开标验证</span>
+                {elseif !empty($manualPicked) /}
+                <span class="audit-bid-open-verified-tag"><i class="fa fa-check-circle"></i> 已指定供应商</span>
+                <span class="audit-manual-pick-inline">
+                    {if !empty($pickedCompanyName)}
+                    <strong>指定供应商:</strong>{$pickedCompanyName|htmlentities}
+                    {/if}
+                    {if !empty($manualPickReason)}
+                    {if !empty($pickedCompanyName)}<span class="audit-manual-pick-inline-sep">|</span>{/if}
+                    <strong>指定说明:</strong>{$manualPickReason|htmlentities}
+                    {/if}
+                </span>
                 {else /}
                 {if $auth->check('procuremen/bidopenverify') || $auth->check('procuremen/bidopen') || $auth->check('procuremen/auditissue')}
                 <button type="button" class="btn btn-warning btn-sm audit-bid-open-btn" id="btn-audit-bid-open">
@@ -367,21 +437,30 @@
                 {/if}
                 {/if}
             </div>
+            {if !$bidOpenVerified && empty($manualPicked)}
+            {if $auth->check('procuremen/auditmanualpick') || $auth->check('procuremen/auditsubmit') || $auth->check('procuremen/auditissue')}
+            <div class="audit-deadline-item audit-manual-pick-item">
+                <button type="button" class="btn btn-success btn-sm audit-manual-pick-btn" id="btn-audit-manual-pick" title="不通过开标验证时,可指定供应商并填写说明">
+                    指定供应商
+                </button>
+            </div>
+            {/if}
+            {/if}
         </div>
     </form>
-<!--    <p class="audit-notify-tip" style="margin-top:-2px;">-->
-<!--        <i class="fa fa-shield"></i>-->
-<!--        未开标验证前,供应商单价与交货日期显示「未开标验证」;请点击「开标」完成双重验证后方可查看报价与交期并确认供应商(验证一次后本单可持续操作)。-->
-<!--    </p>-->
     <p class="audit-notify-tip">
         <i class="fa fa-exclamation-triangle"></i>
-        <strong>重要提示:</strong>可先预选供应商。过招标截止时间后可点「开标」完成验证,再点「确认」。确认后将进入采购终审(本步<strong>不发送短信通知</strong>)。
+        <strong>重要提示:</strong>招标截止时间后可点「开标」完成验证,再点「确认」。可点「指定供应商」并填写选择说明后查看单价。确认后将进入采购终审(本步<strong>不发送短信通知</strong>)。
         需再次提醒报价时,请在右侧<strong>操作</strong>列点击「发邮件」或「发短信」。
     </p>
-    {if !empty($bidOpenVerified)}
+    {if !empty($quoteVisible)}
     <p class="audit-score-rule-tip">
         <i class="fa fa-info-circle"></i>
-        <strong>评分规则:</strong>{$supplierScoreRuleText|default='(总分)=(商务/技术得分×50%)+(价格得分);价格得分=(最低单价合计/本供应商单价合计)×50%×100'|htmlentities}
+        {if !empty($orderScoreRuleName)}
+        <strong>订单类型:</strong>{$orderScoreRuleName|htmlentities}
+        <span class="text-muted">|</span>
+        {/if}
+        <strong>评分规则:</strong>{$supplierScoreRuleText|default='(总分)=(质量得分×50%)+(价格得分);价格得分=(最低单价合计/本供应商单价合计)×50%×100'|htmlentities}
     </p>
     {/if}
     <div class="audit-table-wrap audit-quote-table-wrap">
@@ -394,7 +473,7 @@
                 <th style="width:100px;">邮箱</th>
                 <th style="width:120px;">手机号</th>
                 <th class="text-center col-rank" style="width:64px;">排名</th>
-                <th class="text-center" style="width:88px;">商务/技术得分</th>
+                <th class="text-center" style="width:88px;">质量得分</th>
                 <th class="text-center" style="width:80px;" title="价格得分=(最低单价合计/本供应商单价合计)×价格权重%×100">价格得分</th>
                 <th class="text-center" style="width:72px;">总分</th>
                 <th style="width:130px;">工序名称</th>
@@ -414,17 +493,18 @@
                 <td class="text-center"{if condition="$co.display_rowspan gt 1"} rowspan="{$co.display_rowspan}"{/if} style="vertical-align:middle;">
                     <label>
                         <input type="radio" class="audit-quote-pick" name="audit_quote_pick" value="{$co.name|default=''|htmlentities}"
-                            {if condition="$pickedCompanyName neq '' && $pickedCompanyName eq $co.name"} checked="checked"{/if}/>
+                            {if condition="$pickedCompanyName neq '' && $pickedCompanyName eq $co.name"} checked="checked"{/if}
+                            {if !empty($manualPicked)} disabled="disabled"{/if}/>
                     </label>
                 </td>
                 <td{if condition="$co.display_rowspan gt 1"} rowspan="{$co.display_rowspan}"{/if} style="vertical-align:middle;">{$co.name|default=''|htmlentities}</td>
                 <td{if condition="$co.display_rowspan gt 1"} rowspan="{$co.display_rowspan}"{/if} style="vertical-align:middle;">{$co.username|default=''|htmlentities}</td>
                 <td{if condition="$co.display_rowspan gt 1"} rowspan="{$co.display_rowspan}"{/if} style="vertical-align:middle;">{:htmlentities(mask_email(isset($co['email']) ? $co['email'] : ''))}</td>
                 <td{if condition="$co.display_rowspan gt 1"} rowspan="{$co.display_rowspan}"{/if} style="vertical-align:middle;">{:htmlentities(mask_phone(isset($co['phone']) ? $co['phone'] : ''))}</td>
-                <td class="text-center col-rank"{if condition="$co.display_rowspan gt 1"} rowspan="{$co.display_rowspan}"{/if} style="vertical-align:middle;font-size:17px;font-weight:700;">{if condition="!empty($bidOpenVerified)"}{$co.service_rank_text|default=''|htmlentities}{/if}</td>
-                <td class="text-center"{if condition="$co.display_rowspan gt 1"} rowspan="{$co.display_rowspan}"{/if} style="vertical-align:middle;">{if condition="!empty($bidOpenVerified)"}{$co.service_quality_score_text|default=''|htmlentities}{/if}</td>
-                <td class="text-center"{if condition="$co.display_rowspan gt 1"} rowspan="{$co.display_rowspan}"{/if} style="vertical-align:middle;"{if condition="!empty($bidOpenVerified)"} title="单价合计 {$co.service_price_sum_text|default=''|htmlentities}"{/if}>{if condition="!empty($bidOpenVerified)"}{$co.service_price_score_text|default=''|htmlentities}{/if}</td>
-                <td class="text-center"{if condition="$co.display_rowspan gt 1"} rowspan="{$co.display_rowspan}"{/if} style="vertical-align:middle;font-weight:600;">{if condition="!empty($bidOpenVerified)"}{$co.service_score_text|default=''|htmlentities}{/if}</td>
+                <td class="text-center col-rank"{if condition="$co.display_rowspan gt 1"} rowspan="{$co.display_rowspan}"{/if} style="vertical-align:middle;font-size:17px;font-weight:700;">{if condition="!empty($quoteVisible)"}{$co.service_rank_text|default=''|htmlentities}{/if}</td>
+                <td class="text-center"{if condition="$co.display_rowspan gt 1"} rowspan="{$co.display_rowspan}"{/if} style="vertical-align:middle;">{if condition="!empty($quoteVisible)"}{$co.service_quality_score_text|default=''|htmlentities}{/if}</td>
+                <td class="text-center"{if condition="$co.display_rowspan gt 1"} rowspan="{$co.display_rowspan}"{/if} style="vertical-align:middle;"{if condition="!empty($quoteVisible)"} title="单价合计 {$co.service_price_sum_text|default=''|htmlentities}"{/if}>{if condition="!empty($quoteVisible)"}{$co.service_price_score_text|default=''|htmlentities}{/if}</td>
+                <td class="text-center"{if condition="$co.display_rowspan gt 1"} rowspan="{$co.display_rowspan}"{/if} style="vertical-align:middle;font-weight:600;">{if condition="!empty($quoteVisible)"}{$co.service_score_text|default=''|htmlentities}{/if}</td>
                 {/if}
                 <td>{$ln.cgymc|default=''|htmlentities}</td>
                 <td class="text-center{if condition="!empty($ln.amount_quote_pending)"} audit-quote-pending{elseif condition="$ln.amount_filled neq 1"} audit-quote-empty{/if}">{$ln.unit_price_text|default='未填写'|htmlentities}</td>

+ 9 - 3
application/admin/view/procuremen/details_fragment.html

@@ -403,13 +403,15 @@
     </div>
     {/notempty}
 
-    <div class="section-title"{if !empty($pdf_export)} style="margin:16px 0 10px;font-size:14px;font-weight:600;color:#333;padding-bottom:6px;border-bottom:1px solid #eee;"{/if}>状态进度</div>
+    <div class="section-title"{if !empty($pdf_export)} style="margin:16px 0 10px;font-size:14px;font-weight:600;color:#333;padding-bottom:6px;border-bottom:1px solid #eee;"{/if}>
+        状态进度
+    </div>
     {notempty name="pdf_export"}
     <div class="proc-pdf-steps-outer" style="overflow:hidden;margin:0;padding:0;">
     <table class="proc-steps-table-pdf" width="100%" cellpadding="0" cellspacing="0" style="border-collapse:collapse;table-layout:fixed;width:100%;margin:0 0 18px;">
         <tr>
             {volist name="steps" id="st"}
-            <td style="vertical-align:top;text-align:center;padding:4px 1px 0;border:0 !important;width:16.666%;">
+            <td style="vertical-align:top;text-align:center;padding:4px 1px 0;border:0 !important;width:14.28%;">
                 <table width="100%" cellpadding="0" cellspacing="0" style="border-collapse:collapse;height:152px;margin:0 auto;">
                     <tr>
                         <td style="vertical-align:top;padding:0;border:0 !important;">
@@ -534,6 +536,10 @@
     {notempty name="supplierScoreRuleText"}
     <p class="text-muted" style="margin:-4px 0 8px;font-size:12px;line-height:1.5;">
         <i class="fa fa-info-circle"></i>
+        {notempty name="orderScoreRuleName"}
+        <strong>订单类型:</strong>{$orderScoreRuleName|htmlentities}
+        <span class="text-muted">|</span>
+        {/notempty}
         <strong>评分规则:</strong>{$supplierScoreRuleText|htmlentities}
         <span class="text-muted">(开标后可见)</span>
     </p>
@@ -547,7 +553,7 @@
                 <th class="col-supplier-email">邮箱</th>
                 <th class="col-supplier-phone">手机号</th>
                 <th class="text-center col-rank" style="width:64px;">排名</th>
-                <th class="text-center" style="width:88px;">商务/技术得分</th>
+                <th class="text-center" style="width:88px;">质量得分</th>
                 <th class="text-center" style="width:80px;" title="价格得分=(最低单价合计/本供应商单价合计)×价格权重%×100">价格得分</th>
                 <th class="text-center" style="width:72px;">总分</th>
                 <th style="width:100px;">工序名称</th>

+ 22 - 11
application/admin/view/procuremen/outward_detail.html

@@ -122,6 +122,7 @@
         gap: 10px 20px;
         margin: 0 0 10px;
         font-size: 13px;
+        width: 100%;
     }
     .outward-confirm-deadline-item {
         display: flex;
@@ -135,6 +136,18 @@
         white-space: nowrap;
         margin: 0;
     }
+    .outward-confirm-deadline-item.outward-order-type-item {
+        align-items: center;
+    }
+    .outward-confirm-order-type-text {
+        display: inline-block;
+        min-height: 30px;
+        line-height: 30px;
+        font-size: 13px;
+        font-weight: 600;
+        color: #333;
+        vertical-align: middle;
+    }
     .outward-confirm-delivery-deadline-input {
         width: 148px;
         min-width: 132px;
@@ -274,7 +287,6 @@
                 <col style="width:80px"/>
                 <col style="width:72px"/>
                 <col style="width:72px"/>
-                <col style="width:72px"/>
                 <col style="width:80px"/>
                 <col/>
             </colgroup>
@@ -284,7 +296,6 @@
                 <th>印件名称</th>
                 <th>工序名称</th>
                 <th class="text-center">单位</th>
-                <th class="text-center">工作量</th>
                 <th class="text-center">本次数量</th>
                 <th class="text-center">最高限价</th>
                 <th class="text-center">订法</th>
@@ -300,7 +311,6 @@
                 {/if}
                 <td class="outward-process-name">{$pr.CGYMC|default=''}</td>
                 <td class="text-center">{$pr.CDW|default=''}</td>
-                <td class="text-center">{$pr.NGZL|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>
@@ -331,17 +341,18 @@
             <label class="deadline-label">交货截止日期:</label>
             <input type="text" class="form-control outward-confirm-delivery-deadline-input" value="{$deliveryDeadline|default=''|htmlentities}" title="交货截止日期" readonly="readonly" disabled="disabled" tabindex="-1" placeholder=""/>
         </div>
+        {if !empty($orderScoreRuleName)}
+        <div class="outward-confirm-deadline-item outward-order-type-item">
+            <label class="deadline-label">订单类型:</label>
+            <span class="outward-confirm-order-type-text" title="{$supplierScoreRuleText|default=''|htmlentities}">{$orderScoreRuleName|htmlentities}</span>
+        </div>
+        {/if}
     </div>
 
     <p class="audit-notify-tip">
-        审批通过:将向中标供应商发送「已通过」短信、向未中标供应商发送「未通过」短信。
-    </p>
-    {if !empty($bidOpenVerified)}
-    <p class="audit-score-rule-tip">
-        <i class="fa fa-info-circle"></i>
-        <strong>评分规则:</strong>{$supplierScoreRuleText|default='(总分)=(商务/技术得分×50%)+(价格得分);价格得分=(最低单价合计/本供应商单价合计)×50%×100'|htmlentities}
+        <i class="fa fa-exclamation-triangle"></i>
+        <strong>重要提示:</strong>审批通过:将向中标供应商发送「已通过」短信、向未中标供应商发送「未通过」短信。
     </p>
-    {/if}
     <p class="outward-confirm-block-title">供应商({$confirmPickCount|default=0} 家)</p>
     <div class="outward-confirm-company-wrap">
         <table class="table table-bordered table-condensed outward-confirm-company-table outward-confirm-readonly">
@@ -353,7 +364,7 @@
                 <th style="min-width:160px;">邮箱</th>
                 <th style="width:120px;">手机号</th>
                 <th class="text-center col-rank" style="width:64px;">排名</th>
-                <th class="text-center" style="width:88px;">商务/技术得分</th>
+                <th class="text-center" style="width:88px;">质量得分</th>
                 <th class="text-center" style="width:80px;" title="价格得分=(最低单价合计/本供应商单价合计)×价格权重%×100">价格得分</th>
                 <th class="text-center" style="width:72px;">总分</th>
                 <th style="width:120px;">工序名称</th>

+ 17 - 0
application/admin/view/procuremen/review.html

@@ -177,6 +177,12 @@
         height: 32px;
         padding: 4px 8px;
     }
+    .review-order-type-select {
+        width: 220px;
+        min-width: 180px;
+        height: 32px;
+        padding: 4px 8px;
+    }
     .review-delivery-deadline-input:not(.has-value)::-webkit-datetime-edit,
     .review-delivery-deadline-input:not(.has-value)::-webkit-datetime-edit-fields-wrapper,
     .review-delivery-deadline-input:not(.has-value)::-webkit-datetime-edit-text,
@@ -631,6 +637,17 @@
                     <span class="review-field-label">交货截止日期:<span class="text-danger">*</span></span>
                     <input type="date" class="form-control review-delivery-deadline-input" id="review-delivery-deadline" name="delivery_deadline" title="交货截止日期" autocomplete="off"/>
                 </div>
+                {if condition="isset($pickMode) && $pickMode"}
+                <div class="review-deadline-item">
+                    <span class="review-field-label">订单类型:<span class="text-danger">*</span></span>
+                    <select class="form-control review-order-type-select" id="review-score-rule-id" name="score_rule_id" title="订单类型(评分规则)">
+                        <option value="">请选择订单类型</option>
+                        {volist name="scoreRuleOptions" id="rule"}
+                        <option value="{$rule.id}" data-qw="{$rule.quality_weight}" data-pw="{$rule.price_weight}" data-lw="{$rule.lead_weight}"{if $rule.is_default} selected{/if}>{$rule.name|htmlentities}</option>
+                        {/volist}
+                    </select>
+                </div>
+                {/if}
             </div>
         </div>
         <div class="review-selected-summary" id="review-selected-summary" aria-live="polite">

+ 187 - 5
application/admin/view/procuremen/rfqlist.html

@@ -1,12 +1,194 @@
 <div class="panel panel-default panel-intro procuremen-rfqlist-wrap" data-super-admin="{if condition='$isSuperAdmin'}1{else /}0{/if}">
     {:build_heading()}
+    <style>
+        .procuremen-rfqlist-wrap .procuremen-supplier-lines-cell {
+            position: relative;
+        }
+        .procuremen-rfqlist-wrap .procuremen-supplier-lines-cell .procuremen-supplier-caret {
+            position: absolute;
+            right: 0;
+            top: 0;
+            height: 100%;
+            line-height: 1;
+            background: #eee;
+            color: #ddd;
+            padding: 0 5px;
+            display: flex;
+            justify-content: center;
+            align-items: center;
+            cursor: pointer;
+        }
+        .procuremen-rfqlist-wrap .procuremen-supplier-lines-cell .procuremen-supplier-caret:hover {
+            color: #999;
+        }
+        .procuremen-rfqlist-wrap .bootstrap-table .fixed-table-container thead th,
+        .procuremen-rfqlist-wrap .bootstrap-table .fixed-table-container tbody td {
+            vertical-align: middle !important;
+            overflow: hidden;
+            text-overflow: ellipsis;
+        }
+        /* 仅主表固定布局 */
+        .procuremen-rfqlist-wrap .bootstrap-table > .fixed-table-container > .fixed-table-header > table,
+        .procuremen-rfqlist-wrap .bootstrap-table > .fixed-table-container > .fixed-table-body > table {
+            table-layout: fixed;
+        }
+        /* 操作列吸顶右侧,横向滚动时始终可见 */
+        .procuremen-rfqlist-wrap .bootstrap-table .fixed-table-header th.procuremen-rfq-operate-col,
+        .procuremen-rfqlist-wrap .bootstrap-table .fixed-table-body td.procuremen-rfq-operate-col,
+        .procuremen-rfqlist-wrap .bootstrap-table th[data-field="operate"],
+        .procuremen-rfqlist-wrap .bootstrap-table td[data-field="operate"] {
+            position: sticky;
+            right: 0;
+            background: #fff;
+            z-index: 5;
+            overflow: visible !important;
+            box-shadow: none;
+            border-left: 1px solid #e8e8e8 !important;
+            padding-left: 10px !important;
+            padding-right: 10px !important;
+        }
+        .procuremen-rfqlist-wrap .bootstrap-table .fixed-table-header th.procuremen-rfq-operate-col,
+        .procuremen-rfqlist-wrap .bootstrap-table th[data-field="operate"] {
+            z-index: 6;
+            background: #f5f5f5;
+        }
+        .procuremen-rfqlist-wrap .procuremen-rfq-op-btns {
+            display: inline-flex;
+            flex-wrap: nowrap;
+            align-items: center;
+            justify-content: center;
+            gap: 6px;
+            white-space: nowrap;
+            max-width: 100%;
+        }
+        .procuremen-rfqlist-wrap .procuremen-rfq-op-btns .btn {
+            margin: 0 !important;
+            flex: 0 0 auto;
+        }
+        .procuremen-rfqlist-wrap .bootstrap-table .fixed-table-container thead th.procuremen-rfq-num-col,
+        .procuremen-rfqlist-wrap .bootstrap-table .fixed-table-container tbody td.procuremen-rfq-num-col,
+        .procuremen-rfqlist-wrap .bootstrap-table th[data-field="CDW"],
+        .procuremen-rfqlist-wrap .bootstrap-table td[data-field="CDW"],
+        .procuremen-rfqlist-wrap .bootstrap-table th[data-field="This_quantity"],
+        .procuremen-rfqlist-wrap .bootstrap-table td[data-field="This_quantity"],
+        .procuremen-rfqlist-wrap .bootstrap-table th[data-field="ceilingPrice"],
+        .procuremen-rfqlist-wrap .bootstrap-table td[data-field="ceilingPrice"],
+        .procuremen-rfqlist-wrap .bootstrap-table th[data-field="CDF"],
+        .procuremen-rfqlist-wrap .bootstrap-table td[data-field="CDF"] {
+            text-align: center !important;
+            vertical-align: middle !important;
+        }
+        .procuremen-rfqlist-wrap .bootstrap-table .fixed-table-container thead th.procuremen-rfq-num-col .th-inner,
+        .procuremen-rfqlist-wrap .bootstrap-table th[data-field="This_quantity"] .th-inner,
+        .procuremen-rfqlist-wrap .bootstrap-table th[data-field="ceilingPrice"] .th-inner,
+        .procuremen-rfqlist-wrap .bootstrap-table th[data-field="CDW"] .th-inner,
+        .procuremen-rfqlist-wrap .bootstrap-table th[data-field="CDF"] .th-inner {
+            text-align: center !important;
+        }
+        .procuremen-rfq-notify-wrap {
+            padding: 12px 16px 8px;
+            height: 270px;
+            overflow: auto;
+            box-sizing: border-box;
+        }
+        .procuremen-rfq-notify-wrap table {
+            width: 100%;
+            margin: 0;
+        }
+        .procuremen-rfq-notify-wrap th,
+        .procuremen-rfq-notify-wrap td {
+            padding: 8px 10px;
+            vertical-align: middle !important;
+            border-bottom: 1px solid #eee;
+        }
+        .procuremen-rfq-notify-wrap th {
+            background: #f7f7f7;
+            font-weight: 600;
+        }
+        .procuremen-rfq-notify-empty {
+            padding: 80px 12px;
+            text-align: center;
+            color: #999;
+        }
+        /* 左侧月份栏 */
+        .procuremen-rfqlist-wrap > .panel-body {
+            padding: 4px 0 0 0 !important;
+        }
+        .procuremen-rfqlist-wrap .procuremen-rfq-layout {
+            display: flex !important;
+            flex-direction: row !important;
+            flex-wrap: nowrap !important;
+            align-items: stretch;
+            margin: 0 !important;
+            min-height: calc(100vh - 140px);
+        }
+        .procuremen-rfqlist-wrap .procuremen-sidebar {
+            flex: 0 0 96px;
+            width: 96px;
+            min-width: 96px;
+            max-width: 96px;
+            overflow-x: hidden;
+            overflow-y: auto;
+            border-right: 1px solid #d9d9d9;
+            background: #fff;
+            padding: 4px 0 12px;
+        }
+        .procuremen-rfqlist-wrap .procuremen-sidebar .year-title {
+            font-weight: 600;
+            padding: 8px 4px 4px;
+            margin: 4px 0 2px;
+            font-size: 12px;
+            color: #8c8c8c;
+        }
+        .procuremen-rfqlist-wrap .procuremen-sidebar .procuremen-ym-item {
+            display: block;
+            padding: 7px 4px;
+            margin: 2px 6px;
+            color: #595959;
+            border-radius: 6px;
+            text-decoration: none;
+            font-size: 13px;
+            text-align: center;
+        }
+        .procuremen-rfqlist-wrap .procuremen-sidebar .procuremen-ym-item:hover {
+            background: #f0f0f0;
+            color: #1f1f1f;
+        }
+        .procuremen-rfqlist-wrap .procuremen-sidebar .procuremen-ym-item.active {
+            background: #e6f4ff;
+            color: #1677ff;
+            font-weight: 500;
+        }
+        .procuremen-rfqlist-wrap .procuremen-rfq-main {
+            flex: 1 1 0%;
+            min-width: 0;
+            width: 0;
+            overflow: hidden;
+            padding-left: 8px;
+            box-sizing: border-box;
+        }
+    </style>
 
-    <div class="panel-body">
-        <div class="widget-body no-padding">
-            <div id="toolbar" class="toolbar">
-                <a href="javascript:;" class="btn btn-primary btn-refresh" title="{:__('Refresh')}"><i class="fa fa-refresh"></i> 刷新</a>
+    <div class="panel-body" data-default-ym="{$defaultYm|default=''|htmlentities}">
+        <div class="row procuremen-rfq-layout">
+            <div class="procuremen-sidebar">
+                {foreach name="sidebarYearMonths" item="block"}
+                <div class="procuremen-year-block">
+                    <div class="year-title">{$block.year}年</div>
+                    {foreach name="block.months" item="item"}
+                    <a href="javascript:;" class="procuremen-ym-item{if $item.ym == $defaultYm} active{/if}" data-ym="{$item.ym|htmlentities}">{$item.label|htmlentities}</a>
+                    {/foreach}
+                </div>
+                {/foreach}
+            </div>
+            <div class="procuremen-rfq-main">
+                <div class="widget-body no-padding">
+                    <div id="toolbar" class="toolbar">
+                        <a href="javascript:;" class="btn btn-primary btn-refresh" title="{:__('Refresh')}"><i class="fa fa-refresh"></i> 刷新</a>
+                    </div>
+                    <table id="table" class="table table-striped table-bordered table-hover table-nowrap" width="100%"></table>
+                </div>
             </div>
-            <table id="table" class="table table-striped table-bordered table-hover table-nowrap" width="100%"></table>
         </div>
     </div>
 </div>

+ 1 - 1
application/admin/view/procuremensms/edit.html

@@ -96,7 +96,7 @@
             </div>
         </div>
         <aside class="procuremen-sms-edit-right">
-            <p class="var-guide-title">模版变量<br><span style="font-weight:400;font-size:12px;color:#666;">复制或点击左侧变量名插入到正文(四个场景说明相同,改 model 一处即可)</span></p>
+            <p class="var-guide-title">模版变量<br><span style="font-weight:400;font-size:12px;color:#666;">复制或点击左侧变量名插入到正文(场景说明相同,改 model 一处即可)</span></p>
             <table class="table table-bordered table-condensed var-guide-table">
                 <thead>
                 <tr style="background:#f5f5f5;">

+ 13 - 23
application/admin/view/procuremensms/index.html

@@ -1,5 +1,4 @@
 <style>
-    /* bootstrap-table 表头在 .fixed-table-header 克隆表里,需单独居中 .th-inner */
     .procuremen-sms-template-page .bootstrap-table thead th,
     .procuremen-sms-template-page .fixed-table-header thead th,
     .procuremen-sms-template-page .fixed-table-container thead th {
@@ -11,36 +10,28 @@
     .procuremen-sms-template-page .fixed-table-container thead th .th-inner {
         text-align: center !important;
         justify-content: center !important;
-        padding-left: 8px !important;
-        padding-right: 8px !important;
     }
-    .procuremen-sms-template-table {
-        table-layout: fixed;
-        width: 100% !important;
-    }
-    .procuremen-sms-template-page .bootstrap-table tbody td,
-    .procuremen-sms-template-table tbody td {
-        text-align: center !important;
+    .procuremen-sms-template-page .bootstrap-table tbody td {
         vertical-align: middle !important;
     }
-    .procuremen-sms-template-table .sms-content-cell {
-        text-align: center;
-    }
-    .procuremen-sms-template-table td.col-sms-content,
-    .procuremen-sms-template-table th.col-sms-content {
-        width: 360px;
-        max-width: 360px;
+    .procuremen-sms-template-page .bootstrap-table td[data-field="content"],
+    .procuremen-sms-template-page .bootstrap-table th[data-field="content"] {
         white-space: normal !important;
         word-break: break-word;
     }
-    .procuremen-sms-template-table .sms-content-cell {
+    .procuremen-sms-template-page .sms-content-cell {
+        text-align: left;
         white-space: pre-wrap;
         word-break: break-word;
         line-height: 1.55;
         max-width: 100%;
+        padding: 4px 6px;
     }
-    .procuremen-sms-template-table td[data-field="status"],
-    .procuremen-sms-template-table td[data-field="updatetime"] {
+    .procuremen-sms-template-page .bootstrap-table td[data-field="status"],
+    .procuremen-sms-template-page .bootstrap-table td[data-field="updatetime"],
+    .procuremen-sms-template-page .bootstrap-table td[data-field="title"],
+    .procuremen-sms-template-page .bootstrap-table td[data-field="id"] {
+        text-align: center !important;
         white-space: nowrap !important;
     }
 </style>
@@ -50,10 +41,9 @@
     <div class="panel-body procuremen-sms-template-page">
         <div class="widget-body no-padding">
             <div id="toolbar" class="toolbar">
-                <a href="javascript:;" class="btn btn-primary btn-refresh" title="{:__('Refresh')}"><i class="fa fa-refresh"></i></a>
-<!--                <a href="javascript:;" class="btn btn-success btn-edit btn-disabled disabled {:$auth->check('procuremensms/edit')?'':'hide'}" title="{:__('Edit')}" data-area='["960px","580px"]'><i class="fa fa-pencil"></i> {:__('Edit')}</a>-->
+                <a href="javascript:;" class="btn btn-primary btn-refresh" title="{:__('Refresh')}"><i class="fa fa-refresh"></i> 刷新</a>
             </div>
-            <table id="table" class="table table-striped table-bordered table-hover procuremen-sms-template-table"
+            <table id="table" class="table table-striped table-bordered table-hover"
                    data-operate-edit="{:$auth->check('procuremensms/edit')}"
                    width="100%">
             </table>

+ 11 - 6
application/admin/view/supplierscorerule/add.html

@@ -13,26 +13,31 @@
     margin-left: 140px;
 }
 .supplier-score-rule-form.form-horizontal .ssr-field .form-control {
-    width: 120px;
+    width: 220px;
     max-width: 100%;
 }
 </style>
 <form id="add-form" class="form-horizontal supplier-score-rule-form" role="form" data-toggle="validator" method="POST" action="">
-    <input type="hidden" name="row[status]" value="normal">
     <div class="form-group">
-        <label class="control-label"><span class="text-danger">*</span> 商务/技术分%:</label>
+        <label class="control-label"><span class="text-danger">*</span> 规则名称:</label>
         <div class="ssr-field">
-            <input class="form-control" name="row[quality_weight]" type="number" step="1" min="0" value="" data-rule="required;integer" placeholder="请输入整数,如 50">
+            <input class="form-control" name="row[name]" type="text" value="" data-rule="required" placeholder="如:普通订单(价格优先)" maxlength="100">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label"><span class="text-danger">*</span> 质量分%:</label>
+        <div class="ssr-field">
+            <input class="form-control" name="row[quality_weight]" type="number" step="1" min="0" value="" data-rule="required;integer" placeholder="如 30">
         </div>
     </div>
     <div class="form-group">
         <label class="control-label"><span class="text-danger">*</span> 价格分%:</label>
         <div class="ssr-field">
-            <input class="form-control" name="row[price_weight]" type="number" step="1" min="0" value="" data-rule="required;integer" placeholder="请输入整数,如 50">
+            <input class="form-control" name="row[price_weight]" type="number" step="1" min="0" value="" data-rule="required;integer" placeholder="如 50">
         </div>
     </div>
     <div class="form-group">
-        <label class="control-label">交分%:</label>
+        <label class="control-label">交分%:</label>
         <div class="ssr-field">
             <input class="form-control" name="row[lead_weight]" type="number" step="1" min="0" value="0" data-rule="integer" placeholder="0 表示不参与计算">
             <span class="help-block">填 0 或不填则不参与计算总分</span>

+ 9 - 4
application/admin/view/supplierscorerule/edit.html

@@ -13,14 +13,19 @@
     margin-left: 140px;
 }
 .supplier-score-rule-form.form-horizontal .ssr-field .form-control {
-    width: 120px;
+    width: 220px;
     max-width: 100%;
 }
 </style>
 <form id="edit-form" class="form-horizontal supplier-score-rule-form" role="form" data-toggle="validator" method="POST" action="">
-    <input type="hidden" name="row[status]" value="{$row.status|default='normal'}">
     <div class="form-group">
-        <label class="control-label"><span class="text-danger">*</span> 商务/技术分%:</label>
+        <label class="control-label"><span class="text-danger">*</span> 规则名称:</label>
+        <div class="ssr-field">
+            <input class="form-control" name="row[name]" type="text" value="{$row.name|default=''|htmlentities}" data-rule="required" maxlength="100">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label"><span class="text-danger">*</span> 质量分%:</label>
         <div class="ssr-field">
             <input class="form-control" name="row[quality_weight]" type="number" step="1" min="0" value="{:intval($row['quality_weight'])}" data-rule="required;integer">
         </div>
@@ -32,7 +37,7 @@
         </div>
     </div>
     <div class="form-group">
-        <label class="control-label">交分%:</label>
+        <label class="control-label">交分%:</label>
         <div class="ssr-field">
             <input class="form-control" name="row[lead_weight]" type="number" step="1" min="0" value="{:intval(isset($row['lead_weight']) ? $row['lead_weight'] : 0)}" data-rule="integer">
             <span class="help-block">填 0 或不填则不参与计算总分</span>

+ 2 - 2
application/admin/view/supplierscorerule/index.html

@@ -4,8 +4,8 @@
         <div class="widget-body no-padding">
             <div id="toolbar" class="toolbar">
                 <a href="javascript:;" class="btn btn-primary btn-refresh" title="{:__('Refresh')}"><i class="fa fa-refresh"></i></a>
-                <a href="javascript:;" class="btn btn-success btn-add {:$auth->check('supplierscorerule/add')?'':'hide'}" title="{:__('Add')}" data-area='["460px","360px"]'><i class="fa fa-plus"></i> {:__('Add')}</a>
-                <a href="javascript:;" class="btn btn-success btn-edit btn-disabled disabled {:$auth->check('supplierscorerule/edit')?'':'hide'}" title="{:__('Edit')}" data-area='["460px","360px"]'><i class="fa fa-pencil"></i> {:__('Edit')}</a>
+                <a href="javascript:;" class="btn btn-success btn-add {:$auth->check('supplierscorerule/add')?'':'hide'}" title="{:__('Add')}" data-area='["520px","440px"]'><i class="fa fa-plus"></i> {:__('Add')}</a>
+                <a href="javascript:;" class="btn btn-success btn-edit btn-disabled disabled {:$auth->check('supplierscorerule/edit')?'':'hide'}" title="{:__('Edit')}" data-area='["520px","440px"]'><i class="fa fa-pencil"></i> {:__('Edit')}</a>
                 <a href="javascript:;" class="btn btn-danger btn-del btn-disabled disabled {:$auth->check('supplierscorerule/del')?'':'hide'}" title="{:__('Delete')}"><i class="fa fa-trash"></i> {:__('Delete')}</a>
             </div>
             <table id="table" class="table table-striped table-bordered table-hover table-nowrap"

+ 2 - 1
application/admin/view/supplierservicescore/index.html

@@ -46,6 +46,7 @@
     .supplierservicescore-page .bootstrap-table .table td .ssc-quality-score-input,
     .supplierservicescore-page .bootstrap-table .table td .ssc-price-score-input,
     .supplierservicescore-page .bootstrap-table .table td .ssc-delivery-score-input,
+    .supplierservicescore-page .bootstrap-table .table td .ssc-value-added-score-input,
     .supplierservicescore-page .bootstrap-table .table td .ssc-final-score-input,
     .supplierservicescore-page .bootstrap-table .table td .ssc-performance-score-input {
         display: block !important;
@@ -78,7 +79,7 @@
     <div class="panel-body">
         <div class="widget-body no-padding">
             <div id="toolbar" class="toolbar">
-                <a href="javascript:;" class="btn btn-primary btn-refresh" title="{:__('Refresh')}"><i class="fa fa-refresh"></i></a>
+                <a href="javascript:;" class="btn btn-primary btn-refresh" title="{:__('Refresh')}"><i class="fa fa-refresh"></i> 刷新</a>
                 <span style="display:inline-block;margin-left:10px;vertical-align:middle;">
                     <a href="javascript:;" class="btn btn-success btn-export-review" id="btn-export-review" title="按月份导出供应商评审表">
                         <i class="fa fa-download"></i> 导出供应商评审表

+ 124 - 0
application/admin/view/supplierservicescore/monthorders.html

@@ -0,0 +1,124 @@
+<style>
+    .ssc-month-orders-wrap {
+        padding: 12px 16px 20px;
+    }
+    .ssc-month-orders-head {
+        margin-bottom: 12px;
+        line-height: 1.6;
+    }
+    .ssc-month-orders-head .ssc-meta {
+        color: #666;
+        margin-right: 16px;
+    }
+    .ssc-month-orders-head .ssc-company {
+        font-weight: 600;
+        color: #333;
+    }
+    .ssc-month-orders-table {
+        margin-bottom: 0;
+    }
+    .ssc-month-orders-table > thead > tr > th,
+    .ssc-month-orders-table > tbody > tr > td {
+        text-align: center;
+        vertical-align: middle;
+        white-space: nowrap;
+    }
+    .ssc-month-orders-table .ssc-name-cell {
+        text-align: left;
+        white-space: normal;
+        word-break: break-all;
+        max-width: 220px;
+    }
+    .ssc-month-orders-empty {
+        padding: 48px 16px;
+        text-align: center;
+        color: #999;
+    }
+    .ssc-progress-done {
+        color: #3c763d;
+        font-weight: 600;
+    }
+</style>
+<div class="ssc-month-orders-wrap">
+    <div class="ssc-month-orders-head">
+        <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>
+    {if $orderRows}
+    <div class="table-responsive">
+        <table class="table table-bordered table-striped table-condensed ssc-month-orders-table">
+            <thead>
+            <tr>
+                <th style="width:50px;">序号</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>
+                <th style="width:80px;">操作</th>
+            </tr>
+            </thead>
+            <tbody>
+            {volist name="orderRows" id="row"}
+            <tr>
+                <td>{$row.seq_no}</td>
+                <td>{$row.CCYDH|default=''|htmlentities}</td>
+                <td class="ssc-name-cell">{$row.CYJMC|default=''|htmlentities}</td>
+                <td>{$row.CCLBMMC|default=''|htmlentities}</td>
+                <td class="ssc-name-cell">{$row.CGYMC|default=''|htmlentities}</td>
+                <td>{if $row.rank_no}{$row.rank_no}{else/}—{/if}</td>
+                <td>{$row.quality_score|default=''}</td>
+                <td>{$row.price_score|default=''}</td>
+                <td>{$row.lead_score|default=''}</td>
+                <td>
+                    {if $row.show_bid_result}
+                        {if $row.is_picked}
+                        <span class="label label-success">中标</span>
+                        {else/}
+                        <span class="label label-danger">未中标</span>
+                        {/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.progress_text eq '已完结'"}
+                    <span class="ssc-progress-done">已完结</span>
+                    {else/}
+                    {$row.progress_text|default=''|htmlentities}
+                    {/if}
+                </td>
+                <td>{if $row.issue_time}{$row.issue_time|htmlentities}{else/}—{/if}</td>
+                <td>
+                    {if condition="$row.scydgy_id gt 0"}
+                    <a href="javascript:;" class="btn btn-xs btn-info btn-ssc-order-details"
+                       data-scydgy-id="{$row.scydgy_id}"
+                       title="详情">
+                        <i class="fa fa-file-text-o"></i> 详情
+                    </a>
+                    {else/}
+                    —
+                    {/if}
+                </td>
+            </tr>
+            {/volist}
+            </tbody>
+        </table>
+    </div>
+    {else/}
+    <div class="ssc-month-orders-empty">该供应商本月暂无订单评分记录</div>
+    {/if}
+</div>

+ 49 - 0
application/api/controller/Procuremen.php

@@ -100,6 +100,55 @@ class Procuremen extends Controller
         if ($redis && $dbOk) {
             try {
                 $redis->set($redisKey, json_encode($result, JSON_UNESCAPED_UNICODE));
+                // 同步侧栏小键 + 清/写按月缓存,避免后台首屏/列表解整包
+                $ymSet = [];
+                $byYm = [];
+                foreach ($list ?: [] as $row) {
+                    if (!is_array($row)) {
+                        continue;
+                    }
+                    $t = '';
+                    foreach (['dputrecord', 'dStamp'] as $k) {
+                        $v = trim((string)($row[$k] ?? ''));
+                        if ($v !== '' && strpos($v, '0000-00-00') !== 0) {
+                            $t = $v;
+                            break;
+                        }
+                    }
+                    if ($t !== '' && preg_match('/^([12]\d{3})-(\d{1,2})/', $t, $m)) {
+                        $mo = (int)$m[2];
+                        if ($mo >= 1 && $mo <= 12) {
+                            $ym = $m[1] . '-' . str_pad((string)$mo, 2, '0', STR_PAD_LEFT);
+                            $ymSet[$ym] = true;
+                            $byYm[$ym][] = $row;
+                        }
+                    }
+                }
+                if ($ymSet !== []) {
+                    $redis->set('procuremen_redis_yms', json_encode(array_keys($ymSet), JSON_UNESCAPED_UNICODE));
+                }
+                // 刷新整包时重写近月池,并清隐藏集缓存
+                for ($i = 0; $i < 24; $i++) {
+                    $calYm = date('Y-m', strtotime('-' . $i . ' month', strtotime(date('Y-m-01'))));
+                    try {
+                        $redis->del('procuremen_pick_pool:' . $calYm);
+                    } catch (\Exception $e) {
+                    }
+                }
+                foreach ($byYm as $ym => $rows) {
+                    try {
+                        $redis->setex(
+                            'procuremen_pick_pool:' . $ym,
+                            600,
+                            json_encode(['ok' => 1, 'rows' => $rows], JSON_UNESCAPED_UNICODE)
+                        );
+                    } catch (\Exception $e) {
+                    }
+                }
+                try {
+                    $redis->del('procuremen_pick_hidden_scydgy_v1');
+                } catch (\Exception $e) {
+                }
             } catch (\Exception $e) {
                 // 写缓存失败仍返回本次查询结果
             }

+ 25 - 5
application/common.php

@@ -1037,10 +1037,30 @@ function erp_price($info){
     return $dun;
 }
 
-//连接 Redis
-function redis(){
+//连接 Redis(连接超时务必短,否则 Redis 异常时整站接口会一直转圈)
+function redis()
+{
+    static $handler = null;
+    static $failed = false;
+    if ($failed) {
+        throw new \RuntimeException('Redis unavailable');
+    }
+    if ($handler instanceof \Redis) {
+        return $handler;
+    }
     $redis = new \Redis();
-    //    $redis->connect($this->redis_config['host'], $this->redis_config['port'], $this->redis_config['timeout']);
-    $redis->connect('127.0.0.1','6379',16400,'');
-    return $redis;
+    // 第 3 参为秒;旧值 16400 会导致连不上时页面“永久加载”
+    $ok = false;
+    try {
+        $ok = @$redis->connect('127.0.0.1', 6379, 1.5);
+    } catch (\Throwable $e) {
+        $ok = false;
+    }
+    if (!$ok) {
+        $failed = true;
+        throw new \RuntimeException('Redis connect failed');
+    }
+    $handler = $redis;
+
+    return $handler;
 }

+ 271 - 10
application/common/library/ProcuremenDashboard.php

@@ -40,42 +40,303 @@ class ProcuremenDashboard
     /**
      * 控制台首行 KPI:今日 / 本月
      *
-     * @return array<string, array{today:int,month:int,link:string,link_text:string}>
+     * @return array<string, mixed>
      */
     public static function buildKpiCards(): array
     {
         $todayStart = date('Y-m-d 00:00:00');
-        $todayEnd = date('Y-m-d H:i:s');
+        $todayEnd = date('Y-m-d 23:59:59');
         $monthStart = date('Y-m-01 00:00:00');
         $monthEnd = date('Y-m-t 23:59:59');
 
+        $inboundToday = self::countInboundScoreInRange($todayStart, $todayEnd);
+        $inboundMonth = self::countInboundScoreInRange($monthStart, $monthEnd);
+
         return [
             'supplier' => [
                 'total'     => self::countActiveSuppliers(),
                 'link'      => '',
                 'link_text' => '',
             ],
+            // 兼容旧模板字段(图表面板仍可能引用)
             'confirm' => [
-                'today'     => self::countOperLogDistinctOrdersInRange(['issue_submit'], $todayStart, $todayEnd),
-                'month'     => self::countPendingStageOrdersInMonth('confirm', $monthStart, $monthEnd),
+                'today'     => self::countQuotedPendingConfirmOrders(),
+                'month'     => self::countQuotedPendingConfirmOrders(),
                 'link'      => 'procuremen/audit',
                 'link_text' => '供应商确认',
             ],
             'approval' => [
-                'today'     => self::countOperLogDistinctOrdersInRange(['audit_select'], $todayStart, $todayEnd),
-                'month'     => self::countPendingStageOrdersInMonth('approval', $monthStart, $monthEnd),
-                'link'      => 'procuremen/confirm',
-                'link_text' => '供应商审批',
+                'today'     => $inboundToday['total'],
+                'month'     => $inboundMonth['total'],
+                'link'      => 'procuremenarchive/index',
+                'link_text' => '历史存证档案查询',
             ],
             'completed' => [
-                'today'     => self::countCompletedInRange($todayStart, $todayEnd),
-                'month'     => self::countCompletedInRange($monthStart, $monthEnd),
+                'today'     => self::countNewRfqInRange($todayStart, $todayEnd),
+                'month'     => self::countNewRfqInRange($monthStart, $monthEnd),
+                'link'      => 'procuremen/rfqlist',
+                'link_text' => '查询询价',
+            ],
+            // 新 KPI
+            'import' => [
+                'today'     => self::countImportDemandInRange($todayStart, $todayEnd),
+                'month'     => self::countImportDemandInRange($monthStart, $monthEnd),
+                'link'      => 'procuremen/pick',
+                'link_text' => '采购供应商初选',
+            ],
+            'quote_pending' => [
+                'today'     => self::countQuotedPendingConfirmOrders(),
+                'month'     => self::countQuotedPendingConfirmOrders(),
+                'link'      => 'procuremen/audit',
+                'link_text' => '供应商确认',
+            ],
+            'inbound' => [
+                'today'     => $inboundToday,
+                'month'     => $inboundMonth,
                 'link'      => 'procuremenarchive/index',
                 'link_text' => '历史存证档案查询',
             ],
+            'rfq_new' => [
+                'today'     => self::countNewRfqInRange($todayStart, $todayEnd),
+                'month'     => self::countNewRfqInRange($monthStart, $monthEnd),
+                'link'      => 'procuremen/rfqlist',
+                'link_text' => '查询询价',
+            ],
+            'rfq_quoted' => [
+                'today'     => self::countQuotedRfqInRange($todayStart, $todayEnd),
+                'month'     => self::countQuotedRfqInRange($monthStart, $monthEnd),
+                'link'      => 'procuremen/rfqlist',
+                'link_text' => '查询询价',
+            ],
         ];
     }
 
+    /**
+     * 导入需求:初选池中按提交日期(dputrecord,空则 dStamp)落在区间内的工序条数
+     */
+    protected static function countImportDemandInRange(string $start, string $end): int
+    {
+        try {
+            $timeExpr = 'COALESCE('
+                . "NULLIF(NULLIF(TRIM(CAST(b.dputrecord AS CHAR(32))), ''), '0000-00-00'),"
+                . "NULLIF(NULLIF(TRIM(CAST(a.dStamp AS CHAR(32))), ''), '0000-00-00')"
+                . ')';
+            $n = Db::table('scydgy')
+                ->alias('a')
+                ->join('mcyd b', 'b.ICYDID = a.ICYDID AND b.iStatus >= 10', 'inner')
+                ->where([
+                    'a.bwjg'    => 1,
+                    'a.iEndBz'  => 0,
+                    'a.iType'   => 0,
+                    'a.iStatus' => 10,
+                ])
+                ->whereRaw("({$timeExpr}) >= ? AND ({$timeExpr}) <= ?", [$start, $end])
+                ->count('a.ID');
+
+            return (int)$n;
+        } catch (\Throwable $e) {
+            return 0;
+        }
+    }
+
+    /**
+     * 报价供应商待确认:当前待确认供应商,且至少一家已填单价
+     */
+    protected static function countQuotedPendingConfirmOrders(): int
+    {
+        $rows = self::loadPendingConfirmRows();
+        if ($rows === []) {
+            return 0;
+        }
+        $sidList = [];
+        $orderBySid = [];
+        foreach ($rows as $row) {
+            if (!is_array($row)) {
+                continue;
+            }
+            $sid = (int)($row['scydgy_id'] ?? 0);
+            if ($sid === 0) {
+                continue;
+            }
+            $sidList[$sid] = true;
+            $orderBySid[$sid] = $row;
+        }
+        if ($sidList === []) {
+            return 0;
+        }
+        try {
+            $details = Db::table('purchase_order_detail')
+                ->where('scydgy_id', 'in', array_keys($sidList))
+                ->field('scydgy_id,amount')
+                ->select();
+        } catch (\Throwable $e) {
+            return 0;
+        }
+        if (!is_array($details)) {
+            return 0;
+        }
+        $quotedSids = [];
+        foreach ($details as $d) {
+            if (!is_array($d)) {
+                continue;
+            }
+            $amount = trim((string)($d['amount'] ?? ''));
+            if ($amount === '' || $amount === '0' || $amount === '0.00') {
+                continue;
+            }
+            $sid = (int)($d['scydgy_id'] ?? 0);
+            if ($sid !== 0) {
+                $quotedSids[$sid] = true;
+            }
+        }
+        if ($quotedSids === []) {
+            return 0;
+        }
+        $matched = [];
+        foreach ($quotedSids as $sid => $_) {
+            if (isset($orderBySid[$sid])) {
+                $matched[] = $orderBySid[$sid];
+            }
+        }
+
+        return self::countDistinctOrders($matched);
+    }
+
+    /**
+     * 已入库打分:合格 / 不合格订单数(按订单号去重)
+     *
+     * @return array{pass:int,fail:int,total:int}
+     */
+    protected static function countInboundScoreInRange(string $start, string $end): array
+    {
+        $empty = ['pass' => 0, 'fail' => 0, 'total' => 0];
+        try {
+            $rows = Db::table('purchase_order_inbound_score')
+                ->whereRaw(
+                    '(COALESCE(updatetime, createtime) >= ? AND COALESCE(updatetime, createtime) <= ?)',
+                    [$start, $end]
+                )
+                ->field('ccydh,scydgy_id,result')
+                ->select();
+        } catch (\Throwable $e) {
+            return $empty;
+        }
+        if (!is_array($rows) || $rows === []) {
+            return $empty;
+        }
+        $passKeys = [];
+        $failKeys = [];
+        foreach ($rows as $row) {
+            if (!is_array($row)) {
+                continue;
+            }
+            $result = trim((string)($row['result'] ?? ''));
+            $ccydh = trim((string)($row['ccydh'] ?? ''));
+            $key = $ccydh !== '' ? $ccydh : ('_sid_' . (int)($row['scydgy_id'] ?? 0));
+            if ($result === '合格') {
+                $passKeys[$key] = true;
+            } elseif ($result === '不合格') {
+                $failKeys[$key] = true;
+            }
+        }
+        $pass = count($passKeys);
+        $fail = count($failKeys);
+
+        return [
+            'pass'  => $pass,
+            'fail'  => $fail,
+            'total' => $pass + $fail,
+        ];
+    }
+
+    /**
+     * 新增询价:手工单 scydgy_id&lt;0
+     */
+    protected static function countNewRfqInRange(string $start, string $end): int
+    {
+        try {
+            $expr = self::pickTimeSqlExpr();
+            $query = Db::table('purchase_order')->field('id,CCYDH');
+            self::applyNotDeletedWhere($query);
+            $query->where('scydgy_id', '<', 0);
+            $query->whereRaw("({$expr}) >= ? AND ({$expr}) <= ?", [$start, $end]);
+            $rows = $query->select();
+        } catch (\Throwable $e) {
+            return 0;
+        }
+
+        return self::countDistinctOrders(is_array($rows) ? $rows : []);
+    }
+
+    /**
+     * 供应商已报价询价:区间内新增询价中,至少一家已填单价
+     */
+    protected static function countQuotedRfqInRange(string $start, string $end): int
+    {
+        try {
+            $expr = self::pickTimeSqlExpr();
+            $query = Db::table('purchase_order')->field('id,CCYDH,scydgy_id');
+            self::applyNotDeletedWhere($query);
+            $query->where('scydgy_id', '<', 0);
+            $query->whereRaw("({$expr}) >= ? AND ({$expr}) <= ?", [$start, $end]);
+            $rows = $query->select();
+        } catch (\Throwable $e) {
+            return 0;
+        }
+        if (!is_array($rows) || $rows === []) {
+            return 0;
+        }
+        $sidList = [];
+        $orderBySid = [];
+        foreach ($rows as $row) {
+            if (!is_array($row)) {
+                continue;
+            }
+            $sid = (int)($row['scydgy_id'] ?? 0);
+            if ($sid >= 0) {
+                continue;
+            }
+            $sidList[$sid] = true;
+            $orderBySid[$sid] = $row;
+        }
+        if ($sidList === []) {
+            return 0;
+        }
+        try {
+            $details = Db::table('purchase_order_detail')
+                ->where('scydgy_id', 'in', array_keys($sidList))
+                ->field('scydgy_id,amount')
+                ->select();
+        } catch (\Throwable $e) {
+            return 0;
+        }
+        if (!is_array($details)) {
+            return 0;
+        }
+        $quotedSids = [];
+        foreach ($details as $d) {
+            if (!is_array($d)) {
+                continue;
+            }
+            $amount = trim((string)($d['amount'] ?? ''));
+            if ($amount === '' || $amount === '0' || $amount === '0.00') {
+                continue;
+            }
+            $sid = (int)($d['scydgy_id'] ?? 0);
+            if ($sid < 0) {
+                $quotedSids[$sid] = true;
+            }
+        }
+        $matched = [];
+        foreach ($quotedSids as $sid => $_) {
+            if (isset($orderBySid[$sid])) {
+                $matched[] = $orderBySid[$sid];
+            }
+        }
+
+        return self::countDistinctOrders($matched);
+    }
+
     /**
      * @param string[] $wflowValues
      */

+ 82 - 1
application/common/library/ProcuremenOperLog.php

@@ -33,6 +33,7 @@ class ProcuremenOperLog
             'purchase_confirm'       => '审核确认供应商',
             'purchase_reject'        => '审核驳回',
             'bid_open_verify'        => '开标验证',
+            'manual_pick'            => '指定供应商',
             'audit_append_supplier'  => '补加供应商',
             'audit_resend_sms'       => '重发短信',
             'audit_resend_email'     => '重发邮件',
@@ -40,6 +41,7 @@ class ProcuremenOperLog
             'pick_soft_delete'       => '初选删除',
             'save_qty_price'         => '保存数量限价',
             'mark_complete'          => '直接完结',
+            'inbound_score'          => '入库评分',
             'audit_abandon'          => '审批重新下发',
             'archive_abandon'        => '历史重新下发',
         ];
@@ -58,6 +60,7 @@ class ProcuremenOperLog
             'purchase_confirm'      => ['审核确认供应商', 'purchase_confirm'],
             'purchase_reject'       => ['审核驳回', 'purchase_reject'],
             'bid_open_verify'       => ['开标验证', 'bid_open_verify'],
+            'manual_pick'           => ['指定供应商', 'manual_pick'],
             'audit_append_supplier' => ['补加供应商', 'audit_append_supplier'],
             'audit_resend_sms'      => ['重发短信', 'audit_resend_sms'],
             'audit_resend_email'    => ['重发邮件', 'audit_resend_email'],
@@ -65,6 +68,7 @@ class ProcuremenOperLog
             'pick_soft_delete'      => ['初选删除', 'pick_soft_delete'],
             'save_qty_price'        => ['保存数量限价', 'save_qty_price'],
             'mark_complete'         => ['直接完结', '完结', 'mark_complete'],
+            'inbound_score'         => ['入库评分', '是否合格', 'inbound_score'],
             'audit_abandon'         => ['审批重新下发', '重新下发', 'audit_abandon'],
             'archive_abandon'       => ['历史重新下发', '存证重新下发', 'archive_abandon'],
         ];
@@ -232,6 +236,55 @@ class ProcuremenOperLog
         return '';
     }
 
+    /**
+     * 从已加载的操作日志中取最近时间(避免详情页重复查库)
+     *
+     * @param array<int, array<string, mixed>> $logs
+     * @param string|array<int, string> $actions
+     */
+    public static function resolveLatestTimeFromLogs(array $logs, int $scydgyId, $actions): string
+    {
+        if (!ProcuremenGuard::isValidScydgyId($scydgyId) || $logs === []) {
+            return '';
+        }
+        $acts = self::expandActionQueryValues($actions);
+        if ($acts === []) {
+            return '';
+        }
+        $actSet = [];
+        foreach ($acts as $a) {
+            $actSet[(string)$a] = true;
+            $actSet[self::toActionLabel((string)$a)] = true;
+        }
+        $bestId = -1;
+        $bestText = '';
+        foreach ($logs as $log) {
+            if (!is_array($log)) {
+                continue;
+            }
+            $sid = (int)($log[self::COL_SCYDGY_ID] ?? 0);
+            if ($sid !== $scydgyId) {
+                continue;
+            }
+            $action = trim((string)($log[self::COL_ACTION] ?? ''));
+            if ($action === '' || (!isset($actSet[$action]) && !isset($actSet[self::toActionLabel($action)]))) {
+                continue;
+            }
+            $id = (int)($log['id'] ?? $log['ID'] ?? 0);
+            if ($id < $bestId) {
+                continue;
+            }
+            $text = ProcuremenTime::formatDisplayDateTime($log['createtime'] ?? null);
+            if ($text === '') {
+                continue;
+            }
+            $bestId = $id;
+            $bestText = $text;
+        }
+
+        return $bestText;
+    }
+
     public static function resolveMarkCompleteTime(int $scydgyId): string
     {
         return self::resolveLatestTime($scydgyId, 'mark_complete');
@@ -287,6 +340,12 @@ class ProcuremenOperLog
         }
         $content = trim((string)($lg[self::COL_CONTENT] ?? ''));
         $content = self::stripLeadingActionPrefixes($content, $action);
+        // 入库评分:只展示合格/不合格,去掉工序括号
+        if ($action === '入库评分' || self::toActionLabel((string)($lg[self::COL_ACTION] ?? '')) === '入库评分') {
+            if (preg_match('/(合格|不合格)/u', $content, $m)) {
+                $content = $m[1];
+            }
+        }
         if ($action === '') {
             $action = '操作';
         }
@@ -759,7 +818,19 @@ class ProcuremenOperLog
      */
     protected static function normalizeMergeContentCore(string $action, string $content): string
     {
-        return self::stripLeadingActionPrefixes(trim($content), trim($action));
+        $core = self::stripLeadingActionPrefixes(trim($content), trim($action));
+        $actionLabel = self::toActionLabel(trim($action));
+        // 入库评分:同结果多工序分条 → 只按「合格/不合格」合并
+        if ($actionLabel === '入库评分' || trim($action) === 'inbound_score') {
+            if (preg_match('/^(合格|不合格)/u', $core, $m)) {
+                return $m[1];
+            }
+            if (preg_match('/(合格|不合格)/u', $core, $m)) {
+                return $m[1];
+            }
+        }
+
+        return $core;
     }
 
     /**
@@ -768,6 +839,16 @@ class ProcuremenOperLog
     protected static function buildMergedProcessContent(string $action, string $content, array $processNames): string
     {
         $core = self::normalizeMergeContentCore($action, $content);
+        $actionLabel = self::toActionLabel(trim($action));
+        // 入库评分:只保留合格/不合格,不拼工序名
+        if ($actionLabel === '入库评分' || trim($action) === 'inbound_score') {
+            $result = $core !== '' ? $core : '合格';
+            if (preg_match('/(合格|不合格)/u', $result, $m)) {
+                $result = $m[1];
+            }
+
+            return $result;
+        }
         $procText = implode('、', $processNames);
         if ($procText === '') {
             return $core !== '' ? $core : $content;

Разница между файлами не показана из-за своего большого размера
+ 904 - 97
application/common/library/ProcuremenSupplierScore.php


+ 6 - 2
application/extra/mproc.php

@@ -2,8 +2,10 @@
 
 /**
  * 手机端协助明细(purchase_order_detail)登录与展示
- * 手机验证码:仅 customer 表登记手机号为普通外协(按 customer 公司名筛明细);否则查 admin.mobile 为管理员(看全部、不可改金额/交期);两者皆无则无权限
- * 账号密码:与后台 admin 一致,可看全部,不可改金额/交期
+ * 手机验证码:仅 customer 表登记手机号为普通外协(按 customer 公司名筛明细)
+ * 账号密码:先 customer,未命中再 admin(fa_admin)
+ *   - 供应商(customer)登录后 → 报价列表(可填单价/交期)
+ *   - 管理员(admin)登录后 → 外协入库评分(合格/不合格)
  */
 return [
     // 邮件根地址(正式环境);本地未配或校验不通过时自动用当前访问域名
@@ -24,6 +26,8 @@ return [
     'mock_sms_code' => '',
     // 开标双重验证:可选验证人所属角色组根 id(含全部子组)
     'bid_open_auth_group_root_id' => 10,
+    // 新增询价「通知业务员」:仅该角色组(默认「业务员」id=12)下的管理员
+    'rfq_notify_salesman_group_id' => 12,
 
     // 当前 login.html 对应哪个系统(换登录页同步改这里)
     // collab = 供应链协同;

+ 21 - 0
application/extra/purchase_order_inbound_score_install.sql

@@ -0,0 +1,21 @@
+-- 外协入库评分(手机端管理员填写合格/不合格)
+CREATE TABLE IF NOT EXISTS `purchase_order_inbound_score` (
+  `id` int unsigned NOT NULL AUTO_INCREMENT,
+  `scydgy_id` int NOT NULL DEFAULT 0 COMMENT '工序ID',
+  `purchase_order_id` int unsigned NOT NULL DEFAULT 0 COMMENT 'purchase_order.id',
+  `ccydh` varchar(64) NOT NULL DEFAULT '' COMMENT '订单号',
+  `cyjmc` varchar(255) NOT NULL DEFAULT '' COMMENT '印件名称',
+  `cgymc` varchar(255) NOT NULL DEFAULT '' COMMENT '工序名称',
+  `company_name` varchar(255) NOT NULL DEFAULT '' COMMENT '中标供应商',
+  `result` varchar(16) NOT NULL DEFAULT '' COMMENT '合格/不合格',
+  `admin_id` int unsigned NOT NULL DEFAULT 0,
+  `admin_name` varchar(64) NOT NULL DEFAULT '',
+  `remark` varchar(500) NOT NULL DEFAULT '',
+  `createtime` datetime DEFAULT NULL,
+  `updatetime` datetime DEFAULT NULL,
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uk_scydgy_id` (`scydgy_id`),
+  KEY `idx_ccydh` (`ccydh`),
+  KEY `idx_result` (`result`),
+  KEY `idx_updatetime` (`updatetime`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='外协入库评分';

+ 3 - 0
application/extra/purchase_order_notify_salesman_install.sql

@@ -0,0 +1,3 @@
+-- 手工询价:通知业务员(可多选,顿号拼接)
+ALTER TABLE `purchase_order`
+  ADD COLUMN `notify_salesman` varchar(255) NOT NULL DEFAULT '' COMMENT '通知业务员(可多选)' AFTER `cywyxm`;

+ 25 - 0
application/extra/rfq_salesman_email_template_install.sql

@@ -0,0 +1,25 @@
+-- 查询询价:通知业务员邮箱模版(scene=rfq_salesman_email)
+-- 变量:{contact_name}/{salesman_name}/{cyjmc}/{cgymc}/{this_quantity}/{supplier_name}/{cywyxm}/{email}
+INSERT INTO `purchase_sms_template` (`scene`, `title`, `content`, `remark`, `status`, `createtime`, `updatetime`)
+SELECT
+  'rfq_salesman_email',
+  '询价邮箱通知',
+  '您好,{contact_name}:\n\n有新的协助采购询价需求,请知悉。\n印件名称:{cyjmc}\n工序名称:{cgymc}\n本次数量:{this_quantity}\n供应商:{supplier_name}\n需求发起人:{cywyxm}\n',
+  '查询询价-通知业务员邮箱;模版名称作邮件主题',
+  1,
+  NOW(),
+  NOW()
+FROM DUAL
+WHERE NOT EXISTS (
+  SELECT 1 FROM `purchase_sms_template` WHERE `scene` = 'rfq_salesman_email' LIMIT 1
+);
+
+-- 已有旧模版时:去掉需求编号,补上供应商
+UPDATE `purchase_sms_template`
+SET `content` = '您好,{contact_name}:\n\n有新的协助采购询价需求,请知悉。\n印件名称:{cyjmc}\n工序名称:{cgymc}\n本次数量:{this_quantity}\n供应商:{supplier_name}\n需求发起人:{cywyxm}\n',
+    `updatetime` = NOW()
+WHERE `scene` = 'rfq_salesman_email'
+  AND (
+    `content` LIKE '%需求编号:{ccydh}%'
+    OR (`content` NOT LIKE '%{supplier_name}%' AND `content` NOT LIKE '%供应商:%')
+  );

+ 38 - 0
application/extra/supplier_score_rule_name_install.sql

@@ -0,0 +1,38 @@
+-- 评分规则表补「规则名称/订单类型」并写入三条权重方案
+-- 可在 Navicat 中直接执行
+
+-- 1) 加 name 字段(若已存在会报 Duplicate column,忽略后继续)
+ALTER TABLE `supplier_score_rule`
+  ADD COLUMN `name` varchar(100) NOT NULL DEFAULT '' COMMENT '规则名称/订单类型' AFTER `id`;
+
+-- 2) 对齐/写入三条方案
+UPDATE `supplier_score_rule`
+SET `name`='普通订单(价格优先)', `quality_weight`=30, `price_weight`=50, `lead_weight`=20, `lead_enabled`=1, `status`='normal', `updatetime`=NOW()
+WHERE `id`=1
+   OR (`quality_weight`=30 AND `price_weight`=50 AND `lead_weight`=20);
+
+INSERT INTO `supplier_score_rule`
+(`name`,`quality_weight`,`price_weight`,`lead_weight`,`lead_enabled`,`is_default`,`status`,`createtime`,`updatetime`)
+SELECT '急单(交期优先)', 30, 20, 50, 1, 0, 'normal', NOW(), NOW()
+FROM DUAL
+WHERE NOT EXISTS (SELECT 1 FROM `supplier_score_rule` WHERE `name`='急单(交期优先)');
+
+INSERT INTO `supplier_score_rule`
+(`name`,`quality_weight`,`price_weight`,`lead_weight`,`lead_enabled`,`is_default`,`status`,`createtime`,`updatetime`)
+SELECT '重点产品(质量优先)', 50, 20, 30, 1, 0, 'normal', NOW(), NOW()
+FROM DUAL
+WHERE NOT EXISTS (SELECT 1 FROM `supplier_score_rule` WHERE `name`='重点产品(质量优先)');
+
+-- 3) 非方案旧数据隐藏(例如原先 60/40/0)
+UPDATE `supplier_score_rule`
+SET `status`='hidden', `is_default`=0, `updatetime`=NOW()
+WHERE `status`='normal'
+  AND (`name`='' OR `name` IS NULL OR `name` NOT IN (
+    '普通订单(价格优先)',
+    '急单(交期优先)',
+    '重点产品(质量优先)'
+  ));
+
+-- 4) 默认使用「普通订单(价格优先)」
+UPDATE `supplier_score_rule` SET `is_default`=0;
+UPDATE `supplier_score_rule` SET `is_default`=1 WHERE `name`='普通订单(价格优先)' AND `status`='normal';

+ 2 - 0
application/extra/supplier_service_final_score.sql

@@ -8,6 +8,7 @@ CREATE TABLE IF NOT EXISTS `supplier_service_final_score` (
   `quality_score` decimal(10,2) NOT NULL DEFAULT 0.00 COMMENT '商务技术分(当月订单平均)',
   `price_score` decimal(10,2) NOT NULL DEFAULT 0.00 COMMENT '价格分(当月订单平均)',
   `delivery_score` decimal(10,2) NOT NULL DEFAULT 0.00 COMMENT '交货分',
+  `value_added_score` decimal(10,2) DEFAULT NULL COMMENT '增值服务(人工,空=未填)',
   `final_score` decimal(10,2) DEFAULT NULL COMMENT '最终得分(人工,空=未填)',
   `score_grade` char(1) NOT NULL DEFAULT '' COMMENT '评分等级 A/B/C/D',
   `qp_manual` tinyint unsigned NOT NULL DEFAULT 0 COMMENT '1=商务技术分/价格分/交货分已人工修改',
@@ -22,5 +23,6 @@ CREATE TABLE IF NOT EXISTS `supplier_service_final_score` (
 -- ALTER TABLE `supplier_service_final_score` ADD COLUMN `quality_score` decimal(10,2) NOT NULL DEFAULT 0.00 COMMENT '商务技术分(当月订单平均)' AFTER `score`;
 -- ALTER TABLE `supplier_service_final_score` ADD COLUMN `price_score` decimal(10,2) NOT NULL DEFAULT 0.00 COMMENT '价格分(当月订单平均)' AFTER `quality_score`;
 -- ALTER TABLE `supplier_service_final_score` ADD COLUMN `delivery_score` decimal(10,2) NOT NULL DEFAULT 0.00 COMMENT '交货分' AFTER `price_score`;
+-- ALTER TABLE `supplier_service_final_score` ADD COLUMN `value_added_score` decimal(10,2) DEFAULT NULL COMMENT '增值服务(人工,空=未填)' AFTER `delivery_score`;
 -- ALTER TABLE `supplier_service_final_score` ADD COLUMN `score_grade` char(1) NOT NULL DEFAULT '' COMMENT '评分等级 A/B/C/D' AFTER `final_score`;
 -- ALTER TABLE `supplier_service_final_score` ADD COLUMN `qp_manual` tinyint unsigned NOT NULL DEFAULT 0 COMMENT '1=商务技术分/价格分/交货分已人工修改' AFTER `score_grade`;

+ 793 - 62
application/index/controller/Index.php

@@ -15,7 +15,8 @@ use think\Validate;
 
 /**
  * 手机端:协助明细(purchase_order_detail)验证码 / 账号密码登录 + 列表
- * 普通用户:customer 表(手机号验证码 或 登录账号+密码);管理员:admin 表账号密码(看全部、仅查看)
+ * 普通用户(customer):登录后进报价列表,可填单价/交期
+ * 管理员(admin):登录后进「外协入库评分」,对待入库订单选合格/不合格
  */
 class Index extends Frontend
 {
@@ -125,6 +126,9 @@ class Index extends Frontend
     protected function mprocLoadUserByToken(string $token): ?array
     {
         if ($this->mprocIsSignedAuthToken($token)) {
+            if ($this->mprocIsAuthTokenRevoked($token)) {
+                return null;
+            }
             $user = $this->mprocUserFromSignedToken($token);
             if (!$user) {
                 return null;
@@ -295,8 +299,12 @@ class Index extends Frontend
         if ($raw === null || $raw === '') {
             return null;
         }
+        $raw = (string)$raw;
+        if ($this->mprocIsAuthTokenRevoked($raw)) {
+            return null;
+        }
 
-        return $this->mprocUserFromSignedToken((string)$raw);
+        return $this->mprocUserFromSignedToken($raw);
     }
 
     /**
@@ -324,6 +332,7 @@ class Index extends Frontend
                 'company_name'     => '',
                 'username'         => $uname,
                 'customer_user_id' => 0,
+                'admin_id'         => (int)($row['id'] ?? 0),
                 'login_type'       => 'remember',
                 'is_admin'         => 1,
                 'login_time'       => $loginTime,
@@ -359,21 +368,89 @@ class Index extends Frontend
         return null;
     }
 
+    protected function mprocAuthRevokeCacheKey(string $token): string
+    {
+        return 'mproc_revoked_' . md5($token);
+    }
+
+    protected function mprocRevokeAuthToken(string $token): void
+    {
+        $token = trim($token);
+        if ($token === '') {
+            return;
+        }
+        try {
+            Cache::set($this->mprocAuthRevokeCacheKey($token), 1, $this->mprocTtlSeconds + 86400);
+        } catch (\Throwable $e) {
+        }
+    }
+
+    protected function mprocIsAuthTokenRevoked(string $token): bool
+    {
+        $token = trim($token);
+        if ($token === '') {
+            return false;
+        }
+        try {
+            return (bool)Cache::get($this->mprocAuthRevokeCacheKey($token));
+        } catch (\Throwable $e) {
+            return false;
+        }
+    }
+
+    /**
+     * 按与写入时相同的 path/secure/samesite 清除 Cookie,避免仅 delete 时删不掉
+     */
+    protected function mprocExpireAuthCookies(): void
+    {
+        $opts = $this->mprocCookieOptions();
+        $opts['expire'] = -3600;
+        try {
+            Cookie::set('mproc_token', '', $opts);
+            Cookie::set('mproc_remember', '', $opts);
+        } catch (\Throwable $e) {
+        }
+        try {
+            Cookie::delete('mproc_token');
+            Cookie::delete('mproc_remember');
+        } catch (\Throwable $e) {
+        }
+        unset($_COOKIE['mproc_token'], $_COOKIE['mproc_remember']);
+        $prefix = (string)Config::get('cookie.prefix');
+        if ($prefix !== '') {
+            unset($_COOKIE[$prefix . 'mproc_token'], $_COOKIE[$prefix . 'mproc_remember']);
+        }
+    }
+
     protected function mprocClearLogin($token)
     {
-        if ($token !== null && $token !== '') {
-            Cache::rm($this->mprocAuthCacheKey((string)$token));
-            if (!$this->mprocIsSignedAuthToken((string)$token)) {
-                Cache::rm('mproc_u_' . $token);
+        $token = trim((string)($token ?? ''));
+        if ($token !== '') {
+            $this->mprocRevokeAuthToken($token);
+            try {
+                Cache::rm($this->mprocAuthCacheKey($token));
+            } catch (\Throwable $e) {
+            }
+            if (!$this->mprocIsSignedAuthToken($token)) {
+                try {
+                    Cache::rm('mproc_u_' . $token);
+                } catch (\Throwable $e) {
+                }
+            }
+        }
+        // 同时作废 remember / 另一份 cookie 里可能残留的签名令牌
+        foreach (['mproc_token', 'mproc_remember'] as $ck) {
+            $raw = Cookie::get($ck);
+            if ($raw !== null && $raw !== '' && (string)$raw !== $token) {
+                $this->mprocRevokeAuthToken((string)$raw);
             }
         }
         Session::delete('mproc_token');
-        Cookie::delete('mproc_token');
-        Cookie::delete('mproc_remember');
+        $this->mprocExpireAuthCookies();
     }
 
     /**
-     * 登录成功后的回跳地址校验(仅允许本站「协助明细订单页」路径,防止开放重定向)
+     * 登录成功后的回跳地址校验(仅允许本站手机端首页 / 入库评分页,防止开放重定向)
      *
      * @param string $raw GET/POST 的 redirect 或当前 REQUEST_URI
      */
@@ -407,7 +484,9 @@ class Index extends Frontend
         if (strpos($s, '//') === 0) {
             return '';
         }
-        if (stripos($s, 'index/index/index') === false) {
+        $okHome = stripos($s, 'index/index/index') !== false;
+        $okInbound = stripos($s, 'index/index/inboundscore') !== false;
+        if (!$okHome && !$okInbound) {
             return '';
         }
         if (stripos($s, 'index/index/login') !== false) {
@@ -562,8 +641,8 @@ class Index extends Frontend
             $allowed['main_tab'] = $mt;
         }
         $tb = isset($q['tab']) ? trim((string)$q['tab']) : '';
-        if (in_array($tb, ['draft', 'submitted', 'done', 'me'], true)) {
-            $allowed['tab'] = $tb;
+        if ($tb === 'me' || in_array($this->mprocNormalizeListTab($tb), $this->mprocListTabWhitelist(), true)) {
+            $allowed['tab'] = ($tb === 'me') ? 'me' : $this->mprocNormalizeListTab($tb);
         }
         if (isset($q['q']) && trim((string)$q['q']) !== '') {
             $allowed['q'] = substr(trim((string)$q['q']), 0, 120);
@@ -593,6 +672,36 @@ class Index extends Frontend
         return $base . $path . $qs;
     }
 
+    /**
+     * 按登录身份决定首页:供应商→报价列表;管理员→外协入库评分
+     *
+     * @param array<string, mixed> $user
+     * @param string               $redirectPathOrUrl
+     */
+    protected function mprocBuildAfterLoginHomeUrl(array $user, $redirectPathOrUrl = '')
+    {
+        if (!empty($user['is_admin'])) {
+            $raw = $this->mprocSanitizeRedirectUrl($redirectPathOrUrl);
+            if ($raw !== '' && stripos($raw, 'inboundscore') !== false) {
+                $base = rtrim($this->request->root(true), '/');
+                $path = parse_url($raw, PHP_URL_PATH);
+                $query = parse_url($raw, PHP_URL_QUERY);
+                if (!is_string($path) || $path === '') {
+                    return url('index/index/inboundscore', '', '', true);
+                }
+                if (stripos($path, 'inboundscore') === false) {
+                    return url('index/index/inboundscore', '', '', true);
+                }
+
+                return $base . $path . (is_string($query) && $query !== '' ? ('?' . $query) : '');
+            }
+
+            return url('index/index/inboundscore', '', '', true);
+        }
+
+        return $this->mprocBuildAfterLoginIndexUrl($redirectPathOrUrl);
+    }
+
     /**
      * 从 purchase_order_detail 表解析真实列名(SHOW COLUMNS 只查一次,按候选小写名匹配第一条)
      *
@@ -888,7 +997,7 @@ class Index extends Frontend
         $sessR = $this->mprocSanitizeRedirectUrl((string)Session::get('mproc_intended_url', ''));
         Session::delete('mproc_intended_url');
         $raw = $postR !== '' ? $postR : $sessR;
-        $jump = $this->mprocBuildAfterLoginIndexUrl($raw);
+        $jump = $this->mprocBuildAfterLoginHomeUrl($userData, $raw);
         $this->success('登录成功', $jump, [
             'mproc_token' => $token,
             'keep_hours'  => $this->mprocKeepHours(),
@@ -999,10 +1108,9 @@ class Index extends Frontend
         }
         if ($cn !== '') {
             try {
+                // customer 表无 name 列,勿用 whereOr('name')
                 $hit = Db::table('customer')
-                    ->where(function ($q) use ($cn) {
-                        $q->where('company_name', $cn)->whereOr('name', $cn);
-                    })
+                    ->where('company_name', $cn)
                     ->order('id', 'desc')
                     ->find();
             } catch (\Throwable $e) {
@@ -1652,10 +1760,20 @@ class Index extends Frontend
      */
     protected function mprocAssertQuoteStillEditable(?array $po, string $label = '', ?array $row = null): void
     {
+        $sid = 0;
+        if (is_array($row)) {
+            $sid = (int)($row['scydgy_id'] ?? $row['SCYDGY_ID'] ?? 0);
+        }
+        if ($sid === 0 && is_array($po)) {
+            $sid = (int)($po['scydgy_id'] ?? $po['SCYDGY_ID'] ?? 0);
+        }
+        // 手工询价无招标截止、不开标
+        if ($sid < 0) {
+            return;
+        }
         $this->mprocAssertQuoteNotDeadlineReached($po, $label);
         $freshPo = is_array($po) ? $po : null;
         if (is_array($po)) {
-            $sid = (int)($po['scydgy_id'] ?? $po['SCYDGY_ID'] ?? 0);
             if ($this->mprocIsValidScydgyRowId($sid)) {
                 try {
                     $got = Db::table('purchase_order')->where('scydgy_id', $sid)->find();
@@ -1695,21 +1813,31 @@ class Index extends Frontend
     }
 
     /**
-     * 手机端左侧 Tab:draft|submitted|done
+     * 手机端左侧 Tab:
+     * 询价:rfq / rfq_submitted(手工单 scydgy_id&lt;0,仅未提交/已提交)
+     * 报价:draft / submitted / done(协助下发)
      *
      * @param array<string, mixed>      $row
      * @param array<string, mixed>|null $po
      */
     protected function mprocResolveListTabForRow(array $row, ?array $po, string $effectiveSn): string
     {
-        if (in_array($effectiveSn, ['已完成', '未通过', '已废弃'], true)) {
-            return 'done';
+        $sid = (int)($row['scydgy_id'] ?? $row['SCYDGY_ID'] ?? 0);
+        // 手工询价:无招标截止;仅未提交/已提交(对应待报价/已报价)
+        if ($sid < 0) {
+            if ($effectiveSn === '已提交'
+                || in_array($effectiveSn, ['已完成', '未通过', '已废弃'], true)) {
+                return 'rfq_submitted';
+            }
+
+            return 'rfq';
         }
-        // 已开标或已过招标截止 →「已完成」(含已报价),角标「已截止」或中标结果
-        if ($this->mprocIsBidOpenVerifiedForPoOrRow($po, $row)) {
+        if (in_array($effectiveSn, ['已完成', '未通过', '已废弃'], true)) {
             return 'done';
         }
-        if ($this->mprocIsQuoteDeadlineReachedForPo($po)) {
+        if ($this->mprocIsBidOpenVerifiedForPoOrRow($po, $row)
+            || $this->mprocIsQuoteDeadlineReachedForPo($po)) {
+            // 已开标或已过招标截止 →「已完成」
             return 'done';
         }
         if ($effectiveSn === '已提交') {
@@ -1719,6 +1847,57 @@ class Index extends Frontend
         return 'draft';
     }
 
+    /**
+     * @return string[]
+     */
+    protected function mprocListTabWhitelist(): array
+    {
+        return ['rfq', 'rfq_submitted', 'draft', 'submitted', 'done'];
+    }
+
+    /**
+     * 兼容旧地址 tab=rfq_done → 询价已提交
+     */
+    protected function mprocNormalizeListTab(string $tab): string
+    {
+        if ($tab === 'rfq_done') {
+            return 'rfq_submitted';
+        }
+        if (!in_array($tab, $this->mprocListTabWhitelist(), true)) {
+            return 'draft';
+        }
+
+        return $tab;
+    }
+
+    protected function mprocIsDoneListTab(string $tab): bool
+    {
+        return $tab === 'done';
+    }
+
+    protected function mprocIsRfqListTab(string $tab): bool
+    {
+        return $tab === 'rfq' || $tab === 'rfq_submitted';
+    }
+
+    /**
+     * 复合 Tab → 状态维度:draft|submitted|done
+     */
+    protected function mprocStatusFromListTab(string $tab): string
+    {
+        if ($tab === 'rfq' || $tab === 'draft') {
+            return 'draft';
+        }
+        if ($tab === 'rfq_submitted' || $tab === 'submitted') {
+            return 'submitted';
+        }
+        if ($this->mprocIsDoneListTab($tab)) {
+            return 'done';
+        }
+
+        return 'draft';
+    }
+
     /**
      * 同一订单号 + 供应商合并为一张卡片
      *
@@ -1856,7 +2035,7 @@ class Index extends Frontend
         if ($userWhere !== []) {
             $query->where($userWhere);
         }
-        if (trim((string)$q) === '' && $tab === 'done' && $statusNameCol !== null) {
+        if (trim((string)$q) === '' && $this->mprocIsDoneListTab((string)$tab) && $statusNameCol !== null) {
             $this->mprocSyncLegacyApprovedStatusNames($user, $statusNameCol);
         }
         $this->mprocApplySearchKeywordToDetailQuery($query, $q);
@@ -1944,12 +2123,24 @@ class Index extends Frontend
             }
             $listTab = $this->mprocResolveListTabForRow($row, $poRow, $effectiveSn);
             $row['mproc_list_tab'] = $listTab;
-            $row['mproc_deadline_reached'] = $this->mprocIsQuoteDeadlineReachedForPo($poRow) ? 1 : 0;
-            $row['mproc_pick_result'] = $this->mprocResolvePickResultText($row, $poRow);
-            $row['mproc_bid_open_verified'] = $this->mprocIsBidOpenVerifiedForPoOrRow($poRow, $row) ? 1 : 0;
+            $row['mproc_is_rfq'] = ((int)($row['scydgy_id'] ?? $row['SCYDGY_ID'] ?? 0) < 0) ? 1 : 0;
+            if ((int)$row['mproc_is_rfq'] === 1) {
+                // 手工询价不展示招标/交货截止,也不走开标截止逻辑
+                $row['mproc_bid_deadline'] = '';
+                $row['mproc_bid_deadline_display'] = '';
+                $row['mproc_delivery_deadline'] = '';
+                $row['mproc_delivery_deadline_display'] = '';
+                $row['mproc_deadline_reached'] = 0;
+                $row['mproc_bid_open_verified'] = 0;
+                $row['mproc_pick_result'] = '';
+            } else {
+                $row['mproc_deadline_reached'] = $this->mprocIsQuoteDeadlineReachedForPo($poRow) ? 1 : 0;
+                $row['mproc_pick_result'] = $this->mprocResolvePickResultText($row, $poRow);
+                $row['mproc_bid_open_verified'] = $this->mprocIsBidOpenVerifiedForPoOrRow($poRow, $row) ? 1 : 0;
+            }
             if ($row['mproc_pick_result'] !== '') {
                 $row['mproc_done_label'] = $row['mproc_pick_result'];
-            } elseif ($listTab === 'done' && (
+            } elseif ($this->mprocIsDoneListTab($listTab) && (
                 (int)$row['mproc_deadline_reached'] === 1
                 || (int)$row['mproc_bid_open_verified'] === 1
             )) {
@@ -1982,7 +2173,14 @@ class Index extends Frontend
             $row['amount_missing'] = ($am === null || $am === '' || (is_string($am) && trim($am) === '')) ? 1 : 0;
             $row['delivery_missing'] = ($dv === '' || preg_match('/^0000-00-00/i', $dv)) ? 1 : 0;
             $this->mprocEnrichLeadDaysDisplay($row);
-            $this->mprocEnrichOrderDeadlineDisplay($row);
+            if ((int)$row['mproc_is_rfq'] === 1) {
+                $row['mproc_bid_deadline'] = '';
+                $row['mproc_bid_deadline_display'] = '';
+                $row['mproc_delivery_deadline'] = '';
+                $row['mproc_delivery_deadline_display'] = '';
+            } else {
+                $this->mprocEnrichOrderDeadlineDisplay($row);
+            }
             $row['mproc_fill_hint'] = '';
             $row['mproc_this_quantity_display'] = $this->mprocResolveDisplayThisQuantity($row);
             $row['mproc_remark'] = $this->mprocResolveDetailRemark($row);
@@ -1999,13 +2197,13 @@ class Index extends Frontend
             return is_array($r) && trim((string)($r['mproc_list_tab'] ?? '')) === $tab;
         }));
         // 「已完成」仅保留近三个月(按招标截止日,缺省则交货截止/创建时间)
-        if ($tab === 'done') {
+        if ($this->mprocIsDoneListTab((string)$tab)) {
             $rows = array_values(array_filter($rows, function ($r) {
                 return is_array($r) && $this->mprocIsWithinRecentMonths($r, 3);
             }));
         }
         // 按招标截止日期排序:未提交/已提交截止近的在前;已完成最近截止的在前
-        $rows = $this->mprocSortRowsByBidDeadline($rows, $tab === 'done' ? 'desc' : 'asc');
+        $rows = $this->mprocSortRowsByBidDeadline($rows, $this->mprocIsDoneListTab((string)$tab) ? 'desc' : 'asc');
 
         $groups = $this->mprocGroupRowsByOrder($rows);
 
@@ -2282,7 +2480,7 @@ class Index extends Frontend
         $row['mproc_bid_open_verified'] = $this->mprocIsBidOpenVerifiedForPoOrRow(is_array($poRow) ? $poRow : null, $row) ? 1 : 0;
         if ($row['mproc_pick_result'] !== '') {
             $row['mproc_done_label'] = $row['mproc_pick_result'];
-        } elseif ($listTab === 'done' && (
+        } elseif ($this->mprocIsDoneListTab($listTab) && (
             (int)$row['mproc_deadline_reached'] === 1
             || (int)$row['mproc_bid_open_verified'] === 1
         )) {
@@ -2512,6 +2710,12 @@ class Index extends Frontend
 
             return;
         }
+        // 管理员进外协入库评分,不进供应商报价列表
+        if (!empty($user['is_admin'])) {
+            $this->redirect(url('index/index/inboundscore', '', '', true));
+
+            return;
+        }
         $user = $this->mprocSyncSessionCustomerUser($user);
 
         $tabParamRaw = $this->request->get('tab', null);
@@ -2525,10 +2729,7 @@ class Index extends Frontend
         if (!in_array($mainTab, ['orders', 'me'], true)) {
             $mainTab = 'orders';
         }
-        $tab = $tabParam === 'me' ? 'draft' : $tabParam;
-        if (!in_array($tab, ['draft', 'submitted', 'done'], true)) {
-            $tab = 'draft';
-        }
+        $tab = $tabParam === 'me' ? 'draft' : $this->mprocNormalizeListTab($tabParam);
         $q = trim((string)$this->request->get('q', ''));
 
         $mprocFocusEid = 0;
@@ -2561,6 +2762,8 @@ class Index extends Frontend
 
         $this->view->assign('mprocMainTab', $mainTab);
         $this->view->assign('mprocTab', $tab);
+        $this->view->assign('mprocIsRfqTab', $this->mprocIsRfqListTab($tab) ? 1 : 0);
+        $this->view->assign('mprocStatusTab', $this->mprocStatusFromListTab($tab));
         $this->view->assign('mprocSearchQ', $q);
         $this->view->assign('mprocProfile', $profile);
         $this->view->assign('mprocIsAdmin', !empty($user['is_admin']) ? 1 : 0);
@@ -2607,10 +2810,7 @@ class Index extends Frontend
         if (!in_array($mainTab, ['orders', 'me'], true)) {
             $mainTab = 'orders';
         }
-        $tab = $tabParam === 'me' ? 'draft' : $tabParam;
-        if (!in_array($tab, ['draft', 'submitted', 'done'], true)) {
-            $tab = 'draft';
-        }
+        $tab = $tabParam === 'me' ? 'draft' : $this->mprocNormalizeListTab($tabParam);
         $q = trim((string)$this->request->request('q', ''));
 
         if ($mainTab === 'me') {
@@ -2658,8 +2858,9 @@ class Index extends Frontend
     public function login()
     {
         $redirect = $this->mprocSanitizeRedirectUrl($this->request->get('redirect', ''));
-        if ($this->mprocGetUser()) {
-            $this->redirect($this->mprocBuildAfterLoginIndexUrl($redirect));
+        $user = $this->mprocGetUser();
+        if ($user) {
+            $this->redirect($this->mprocBuildAfterLoginHomeUrl($user, $redirect));
         }
         if ($redirect !== '') {
             Session::set('mproc_intended_url', $redirect);
@@ -2788,7 +2989,7 @@ class Index extends Frontend
         }
         $token = $this->mprocTouchLoginState($user, $token !== '' ? $token : $this->mprocPackSignedAuthToken($user));
         $redirect = $this->mprocSanitizeRedirectUrl($this->request->post('redirect', ''));
-        $jump = $this->mprocBuildAfterLoginIndexUrl($redirect);
+        $jump = $this->mprocBuildAfterLoginHomeUrl($user, $redirect);
         $this->success('ok', $jump, [
             'mproc_token' => $token,
             'keep_hours'  => $this->mprocKeepHours(),
@@ -2823,7 +3024,7 @@ class Index extends Frontend
         $row = null;
         try {
             $row = Db::name('admin')
-                ->field('id,username,password,salt,status,loginfailure,updatetime')
+                ->field('id,username,password,salt,status,loginfailure,updatetime,mobile')
                 ->where('username', $username)
                 ->find();
         } catch (\Throwable $e) {
@@ -2871,6 +3072,7 @@ class Index extends Frontend
             'company_name'     => '',
             'username'         => $username,
             'customer_user_id' => 0,
+            'admin_id'         => $id,
             'login_type'       => 'pwd',
             'is_admin'         => 1,
         ]);
@@ -2889,11 +3091,15 @@ class Index extends Frontend
         if (in_array($sn, ['已完成', '未通过', '已废弃'], true)) {
             return false;
         }
-        if ($this->mprocIsQuoteDeadlineReachedForPo($po)) {
-            return false;
-        }
-        if ($this->mprocIsBidOpenVerifiedForPoOrRow($po, $row)) {
-            return false;
+        $sid = (int)($row['scydgy_id'] ?? $row['SCYDGY_ID'] ?? 0);
+        // 手工询价无招标截止/开标限制
+        if ($sid >= 0) {
+            if ($this->mprocIsQuoteDeadlineReachedForPo($po)) {
+                return false;
+            }
+            if ($this->mprocIsBidOpenVerifiedForPoOrRow($po, $row)) {
+                return false;
+            }
         }
         $uCo = trim((string)($user['company_name'] ?? ''));
         if ($uCo === '') {
@@ -3108,18 +3314,26 @@ class Index extends Frontend
         $hasAmt = $amountRaw !== '';
         $hasLead = $leadRaw !== '';
         $hasDel = $deliveryRaw !== '';
-        $filledCount = ($hasAmt ? 1 : 0) + ($hasLead ? 1 : 0) + ($hasDel ? 1 : 0);
-        if ($filledCount === 0) {
-            throw new \InvalidArgumentException($label . '请填写单价、工期、交期');
-        }
-        if ($filledCount < 3) {
+        $isManualRfq = ($sid < 0);
+        if ($isManualRfq) {
+            // 手工询价:只需填写单价
             if (!$hasAmt) {
                 throw new \InvalidArgumentException($label . '请填写单价');
             }
-            if (!$hasLead) {
-                throw new \InvalidArgumentException($label . '请填写工期');
+        } else {
+            $filledCount = ($hasAmt ? 1 : 0) + ($hasLead ? 1 : 0) + ($hasDel ? 1 : 0);
+            if ($filledCount === 0) {
+                throw new \InvalidArgumentException($label . '请填写单价、工期、交期');
+            }
+            if ($filledCount < 3) {
+                if (!$hasAmt) {
+                    throw new \InvalidArgumentException($label . '请填写单价');
+                }
+                if (!$hasLead) {
+                    throw new \InvalidArgumentException($label . '请填写工期');
+                }
+                throw new \InvalidArgumentException($label . '请填写交期');
             }
-            throw new \InvalidArgumentException($label . '请填写交期');
         }
 
         $leadDays = null;
@@ -3186,8 +3400,18 @@ class Index extends Frontend
         $data['lead_days'] = $leadDays;
 
         $dcCol = $this->mprocResolveProcuremenColumn(['delivery_createtime', 'deliverycreatetime']);
-        if ($dcCol !== null && array_key_exists('delivery', $data) && $data['delivery'] !== null && $data['delivery'] !== '') {
-            $data[$dcCol] = date('Y-m-d H:i:s');
+        if ($dcCol !== null) {
+            $shouldStampQuoteTime = false;
+            if (array_key_exists('delivery', $data) && $data['delivery'] !== null && $data['delivery'] !== '') {
+                $shouldStampQuoteTime = true;
+            }
+            // 手工询价只填单价:有单价也记报价时间
+            if ($isManualRfq && array_key_exists('amount', $data) && $data['amount'] !== null && $data['amount'] !== '') {
+                $shouldStampQuoteTime = true;
+            }
+            if ($shouldStampQuoteTime) {
+                $data[$dcCol] = date('Y-m-d H:i:s');
+            }
         }
         $upCol = $this->mprocResolveProcuremenColumn(['updatetime']);
         if ($upCol !== null) {
@@ -3495,7 +3719,10 @@ class Index extends Frontend
         if ($token === null || $token === '') {
             $token = Cookie::get('mproc_token');
         }
-        $this->mprocClearLogin(preg_replace('/[^a-f0-9]/i', '', (string)$token));
+        if ($token === null || $token === '') {
+            $token = Cookie::get('mproc_remember');
+        }
+        $this->mprocClearLogin((string)$token);
         $this->success('密码已修改,请重新登录', url('index/index/login'));
     }
 
@@ -3508,9 +3735,11 @@ class Index extends Frontend
         if ($token === null || $token === '') {
             $token = Cookie::get('mproc_token');
         }
-        if ($token) {
-            $this->mprocClearLogin(preg_replace('/[^a-f0-9]/i', '', (string)$token));
+        if ($token === null || $token === '') {
+            $token = Cookie::get('mproc_remember');
         }
+        // 无论有无 token 都清 Cookie/Session,避免「记住登录」把用户又带回来
+        $this->mprocClearLogin((string)$token);
         $this->redirect(url('index/index/login'));
     }
 
@@ -3560,4 +3789,506 @@ class Index extends Frontend
         }
         Log::record('smsbao 发送成功 phone=' . $phone . ' content=' . $content, 'info');
     }
+
+    /**
+     * 外协入库评分页(仅管理员)
+     */
+    public function inboundscore()
+    {
+        $user = $this->mprocGetUser();
+        if (!$user) {
+            $uri = isset($_SERVER['REQUEST_URI']) ? (string)$_SERVER['REQUEST_URI'] : '';
+            $safe = $this->mprocSanitizeRedirectUrl($uri);
+            if ($safe !== '') {
+                Session::set('mproc_intended_url', $safe);
+            }
+            $this->redirect($this->mprocBuildLoginUrl($safe));
+
+            return;
+        }
+        if (empty($user['is_admin'])) {
+            $this->redirect(url('index/index/index', '', '', true));
+
+            return;
+        }
+        $this->mprocEnsureInboundScoreTable();
+        $q = trim((string)$this->request->get('q', ''));
+        $profile = $this->mprocProfileForUser($user);
+        $this->view->assign('mprocSearchQ', $q);
+        $this->view->assign('mprocProfile', $profile);
+        $this->view->assign('mprocBootstrapToken', trim((string)($user['token'] ?? '')));
+        $this->view->assign('mprocBootstrapKeepHours', $this->mprocKeepHours());
+        $this->view->assign('rows', $this->mprocLoadInboundScoreRows('pending', $q, (int)($user['admin_id'] ?? 0)));
+
+        return $this->view->fetch();
+    }
+
+    /**
+     * 外协入库评分列表 JSON(仅管理员;tab=pending|pass|fail)
+     * pass/fail 仅返回当前账号操作过的记录
+     */
+    public function inboundscorelist()
+    {
+        $user = $this->mprocRequireAdminUser();
+        $this->mprocEnsureInboundScoreTable();
+        $q = trim((string)$this->request->request('q', ''));
+        $tab = trim((string)$this->request->request('tab', 'pending'));
+        if (!in_array($tab, ['pending', 'pass', 'fail'], true)) {
+            $tab = 'pending';
+        }
+        $adminId = (int)($user['admin_id'] ?? 0);
+        $rows = $this->mprocLoadInboundScoreRows($tab, $q, $adminId);
+        $this->success('ok', '', [
+            'tab'   => $tab,
+            'q'     => $q,
+            'rows'  => $rows,
+            'count' => count($rows),
+        ]);
+    }
+
+    /**
+     * 保存外协入库评分(仅管理员,POST:ccydh 或 scydgy_id、result=合格|不合格)
+     * 同一订单号下多道工序一并评分、一并完结(与历史存证一单一行一致)
+     */
+    public function inboundscoresave()
+    {
+        $user = $this->mprocRequireAdminUser();
+        if (!$this->request->isPost()) {
+            $this->error('请使用 POST');
+        }
+        $this->mprocEnsureInboundScoreTable();
+        $ccydh = trim((string)$this->request->post('ccydh', ''));
+        $sid = (int)$this->request->post('scydgy_id', 0);
+        $result = trim((string)$this->request->post('result', ''));
+        if ($result !== '合格' && $result !== '不合格') {
+            $this->error('请选择合格或不合格');
+        }
+        $approvedVals = ProcuremenStatus::wflowApprovedValues();
+        $approvedIn = implode(',', array_map(static function ($v) {
+            return "'" . str_replace("'", "''", (string)$v) . "'";
+        }, $approvedVals));
+        try {
+            $q = Db::table('purchase_order')
+                ->where('scydgy_id', '>', 0)
+                ->whereRaw('(mod_rq IS NULL OR TRIM(CAST(mod_rq AS CHAR(32))) = \'\' OR TRIM(CAST(mod_rq AS CHAR(32))) LIKE \'0000-00-00%\')')
+                ->whereRaw("pick_time IS NOT NULL AND CAST(pick_time AS CHAR) NOT LIKE '0000-00-00%' AND TRIM(CAST(pick_time AS CHAR)) <> ''")
+                ->whereRaw("pick_company_name IS NOT NULL AND TRIM(pick_company_name) <> ''")
+                ->whereRaw('TRIM(CAST(wflow_status AS CHAR)) IN (' . $approvedIn . ')')
+                ->field('id,scydgy_id,CCYDH,CYJMC,CGYMC,pick_company_name,pick_time,wflow_status,status');
+            if ($ccydh !== '') {
+                $q->where('CCYDH', $ccydh);
+            } elseif ($sid > 0) {
+                $one = Db::table('purchase_order')->where('scydgy_id', $sid)->field('CCYDH')->find();
+                $ccydh = is_array($one) ? trim((string)($one['CCYDH'] ?? '')) : '';
+                if ($ccydh === '') {
+                    $this->error('缺少订单号');
+                }
+                $q->where('CCYDH', $ccydh);
+            } else {
+                $this->error('缺少订单号');
+            }
+            $poRows = $q->order('id', 'asc')->select();
+        } catch (\Throwable $e) {
+            $poRows = [];
+        }
+        if (!is_array($poRows) || $poRows === []) {
+            $this->error('未找到可评分的已审核订单');
+        }
+        $now = date('Y-m-d H:i:s');
+        $adminId = (int)($user['admin_id'] ?? 0);
+        // 操作日志记昵称,不用登录账号
+        $adminName = '';
+        if ($adminId > 0) {
+            try {
+                $adminRow = Db::name('admin')->where('id', $adminId)->field('nickname,username')->find();
+                if (is_array($adminRow)) {
+                    $adminName = trim((string)($adminRow['nickname'] ?? ''));
+                    if ($adminName === '') {
+                        $adminName = trim((string)($adminRow['username'] ?? ''));
+                    }
+                }
+            } catch (\Throwable $e) {
+            }
+        }
+        if ($adminName === '') {
+            $adminName = trim((string)($user['username'] ?? ''));
+        }
+        if ($adminName === '') {
+            $adminName = '管理员';
+        }
+        $saved = 0;
+        $firstSid = 0;
+        $firstPoId = 0;
+        try {
+            Db::startTrans();
+            foreach ($poRows as $po) {
+                if (!is_array($po)) {
+                    continue;
+                }
+                $rowSid = (int)($po['scydgy_id'] ?? 0);
+                if ($rowSid <= 0) {
+                    continue;
+                }
+                if ($firstSid <= 0) {
+                    $firstSid = $rowSid;
+                    $firstPoId = (int)($po['id'] ?? 0);
+                }
+                $g = trim((string)($po['CGYMC'] ?? ''));
+                $data = [
+                    'scydgy_id'         => $rowSid,
+                    'purchase_order_id' => (int)($po['id'] ?? 0),
+                    'ccydh'             => trim((string)($po['CCYDH'] ?? $ccydh)),
+                    'cyjmc'             => trim((string)($po['CYJMC'] ?? '')),
+                    'cgymc'             => $g,
+                    'company_name'      => trim((string)($po['pick_company_name'] ?? '')),
+                    'result'            => $result,
+                    'admin_id'          => $adminId,
+                    'admin_name'        => $adminName,
+                    'updatetime'        => $now,
+                ];
+                $exist = Db::table('purchase_order_inbound_score')->where('scydgy_id', $rowSid)->find();
+                if (is_array($exist) && $exist !== []) {
+                    Db::table('purchase_order_inbound_score')->where('scydgy_id', $rowSid)->update($data);
+                } else {
+                    $data['createtime'] = $now;
+                    Db::table('purchase_order_inbound_score')->insert($data);
+                }
+                Db::table('purchase_order')->where('scydgy_id', $rowSid)->update([
+                    'status' => ProcuremenStatus::PO_COMPLETED,
+                ]);
+                $saved++;
+            }
+            if ($saved < 1) {
+                throw new \RuntimeException('没有可保存的工序');
+            }
+            Db::commit();
+        } catch (\Throwable $e) {
+            try {
+                Db::rollback();
+            } catch (\Throwable $e2) {
+            }
+            Log::record('inboundscoresave fail: ' . $e->getMessage(), 'error');
+            $this->error('保存失败,请稍后重试');
+        }
+        \app\common\library\ProcuremenOperLog::write(
+            $firstSid,
+            'inbound_score',
+            '入库评分:' . $result,
+            $firstPoId > 0 ? $firstPoId : null,
+            [$adminId, $adminName]
+        );
+        // 按入库合格/不合格重算该供应商当月质量得分
+        $companyName = '';
+        foreach ($poRows as $po) {
+            if (!is_array($po)) {
+                continue;
+            }
+            $companyName = trim((string)($po['pick_company_name'] ?? ''));
+            if ($companyName !== '') {
+                break;
+            }
+        }
+        $scoreYm = date('Y-m', strtotime($now) ?: time());
+        if ($companyName !== '') {
+            try {
+                \app\common\library\ProcuremenSupplierScore::syncQualityScoreFromInbound($scoreYm, $companyName);
+            } catch (\Throwable $e) {
+                Log::record('inboundscoresave quality sync: ' . $e->getMessage(), 'error');
+            }
+        }
+        $this->success('操作成功', '', [
+            'ccydh'  => $ccydh,
+            'result' => $result,
+            'count'  => $saved,
+            'status' => '已完结',
+        ]);
+    }
+
+    /**
+     * @return array<string, mixed>
+     */
+    protected function mprocRequireAdminUser(): array
+    {
+        $user = $this->mprocGetUser();
+        if (!$user) {
+            $this->error('请先登录', url('index/index/login'));
+        }
+        if (empty($user['is_admin'])) {
+            $this->error('仅系统管理员可操作');
+        }
+
+        return $user;
+    }
+
+    protected function mprocEnsureInboundScoreTable(): void
+    {
+        static $ok = false;
+        if ($ok) {
+            return;
+        }
+        try {
+            Db::query('SELECT 1 FROM `purchase_order_inbound_score` LIMIT 1');
+            $ok = true;
+
+            return;
+        } catch (\Throwable $e) {
+        }
+        try {
+            Db::execute("CREATE TABLE IF NOT EXISTS `purchase_order_inbound_score` (
+  `id` int unsigned NOT NULL AUTO_INCREMENT,
+  `scydgy_id` int NOT NULL DEFAULT 0 COMMENT '工序ID',
+  `purchase_order_id` int unsigned NOT NULL DEFAULT 0 COMMENT 'purchase_order.id',
+  `ccydh` varchar(64) NOT NULL DEFAULT '' COMMENT '订单号',
+  `cyjmc` varchar(255) NOT NULL DEFAULT '' COMMENT '印件名称',
+  `cgymc` varchar(255) NOT NULL DEFAULT '' COMMENT '工序名称',
+  `company_name` varchar(255) NOT NULL DEFAULT '' COMMENT '中标供应商',
+  `result` varchar(16) NOT NULL DEFAULT '' COMMENT '合格/不合格',
+  `admin_id` int unsigned NOT NULL DEFAULT 0,
+  `admin_name` varchar(64) NOT NULL DEFAULT '',
+  `remark` varchar(500) NOT NULL DEFAULT '',
+  `createtime` datetime DEFAULT NULL,
+  `updatetime` datetime DEFAULT NULL,
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uk_scydgy_id` (`scydgy_id`),
+  KEY `idx_ccydh` (`ccydh`),
+  KEY `idx_result` (`result`),
+  KEY `idx_updatetime` (`updatetime`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='外协入库评分'");
+            $ok = true;
+        } catch (\Throwable $e) {
+            Log::record('mprocEnsureInboundScoreTable: ' . $e->getMessage(), 'error');
+        }
+    }
+
+    /**
+     * 外协入库评分列表:按订单号合并(一单一行,工序顿号合并)
+     * tab=pending 待评分;pass/fail 仅当前管理员操作过的合格/不合格
+     *
+     * @return array<int, array<string, mixed>>
+     */
+    protected function mprocLoadInboundScoreRows(string $tab, string $keyword = '', int $adminId = 0): array
+    {
+        $this->mprocEnsureInboundScoreTable();
+        $keyword = trim($keyword);
+        if (!in_array($tab, ['pending', 'pass', 'fail'], true)) {
+            $tab = 'pending';
+        }
+        try {
+            $approvedVals = ProcuremenStatus::wflowApprovedValues();
+            $approvedIn = implode(',', array_map(static function ($v) {
+                return "'" . str_replace("'", "''", (string)$v) . "'";
+            }, $approvedVals));
+            $poRows = Db::table('purchase_order')
+                ->where('scydgy_id', '>', 0)
+                ->whereRaw('(mod_rq IS NULL OR TRIM(CAST(mod_rq AS CHAR(32))) = \'\' OR TRIM(CAST(mod_rq AS CHAR(32))) LIKE \'0000-00-00%\')')
+                ->whereRaw("pick_time IS NOT NULL AND CAST(pick_time AS CHAR) NOT LIKE '0000-00-00%' AND TRIM(CAST(pick_time AS CHAR)) <> ''")
+                ->whereRaw("pick_company_name IS NOT NULL AND TRIM(pick_company_name) <> ''")
+                ->whereRaw('TRIM(CAST(wflow_status AS CHAR)) IN (' . $approvedIn . ')')
+                ->field('id,scydgy_id,CCYDH,CYJMC,CGYMC,CCLBMMC,CDW,This_quantity,pick_company_name,pick_time,wflow_status,status')
+                ->order('pick_time', 'desc')
+                ->order('id', 'asc')
+                ->limit(800)
+                ->select();
+        } catch (\Throwable $e) {
+            Log::record('mprocLoadInboundScoreRows: ' . $e->getMessage(), 'error');
+
+            return [];
+        }
+        if (!is_array($poRows) || $poRows === []) {
+            return [];
+        }
+        $sids = [];
+        foreach ($poRows as $po) {
+            if (!is_array($po)) {
+                continue;
+            }
+            $sid = (int)($po['scydgy_id'] ?? 0);
+            if ($sid > 0) {
+                $sids[$sid] = true;
+            }
+        }
+        $scoreMap = [];
+        if ($sids !== []) {
+            try {
+                $scores = Db::table('purchase_order_inbound_score')
+                    ->where('scydgy_id', 'in', array_keys($sids))
+                    ->field('scydgy_id,result,admin_id,admin_name,updatetime,createtime')
+                    ->select();
+            } catch (\Throwable $e) {
+                $scores = [];
+            }
+            if (is_array($scores)) {
+                foreach ($scores as $sc) {
+                    if (!is_array($sc)) {
+                        continue;
+                    }
+                    $sid = (int)($sc['scydgy_id'] ?? 0);
+                    if ($sid > 0) {
+                        $scoreMap[$sid] = $sc;
+                    }
+                }
+            }
+        }
+
+        /** @var array<string, array<string, mixed>> $groups */
+        $groups = [];
+        foreach ($poRows as $po) {
+            if (!is_array($po)) {
+                continue;
+            }
+            $sid = (int)($po['scydgy_id'] ?? 0);
+            $ccydh = trim((string)($po['CCYDH'] ?? ''));
+            if ($sid <= 0 || $ccydh === '') {
+                continue;
+            }
+            if (!isset($groups[$ccydh])) {
+                $pickRaw = trim((string)($po['pick_time'] ?? ''));
+                $pickDisp = $pickRaw !== '' ? \app\common\library\ProcuremenTime::formatDisplayDateTime($pickRaw) : '';
+                $groups[$ccydh] = [
+                    'CCYDH'             => $ccydh,
+                    'scydgy_id'         => $sid,
+                    'scydgy_ids'        => [],
+                    'CYJMC'             => trim((string)($po['CYJMC'] ?? '')),
+                    'CCLBMMC'           => trim((string)($po['CCLBMMC'] ?? '')),
+                    'CGYMC_list'        => [],
+                    'pick_company_name' => trim((string)($po['pick_company_name'] ?? '')),
+                    'pick_time'         => $pickDisp !== '' ? $pickDisp : $pickRaw,
+                    'pick_time_raw'     => $pickRaw,
+                    'process_count'     => 0,
+                    'results'           => [],
+                    'score_admin_ids'   => [],
+                    'score_time_raw'    => '',
+                ];
+            }
+            $g = &$groups[$ccydh];
+            $g['scydgy_ids'][] = $sid;
+            $g['process_count']++;
+            if ($g['scydgy_id'] <= 0) {
+                $g['scydgy_id'] = $sid;
+            }
+            if ($g['CYJMC'] === '') {
+                $g['CYJMC'] = trim((string)($po['CYJMC'] ?? ''));
+            }
+            if ($g['CCLBMMC'] === '') {
+                $g['CCLBMMC'] = trim((string)($po['CCLBMMC'] ?? ''));
+            }
+            if ($g['pick_company_name'] === '') {
+                $g['pick_company_name'] = trim((string)($po['pick_company_name'] ?? ''));
+            }
+            $pt = trim((string)($po['pick_time'] ?? ''));
+            if ($pt !== '' && ($g['pick_time_raw'] === '' || strcmp($pt, $g['pick_time_raw']) > 0)) {
+                $g['pick_time_raw'] = $pt;
+                $disp = \app\common\library\ProcuremenTime::formatDisplayDateTime($pt);
+                $g['pick_time'] = $disp !== '' ? $disp : $pt;
+            }
+            $gymc = trim((string)($po['CGYMC'] ?? ''));
+            if ($gymc !== '' && !in_array($gymc, $g['CGYMC_list'], true)) {
+                $g['CGYMC_list'][] = $gymc;
+            }
+            $sc = $scoreMap[$sid] ?? null;
+            $res = is_array($sc) ? trim((string)($sc['result'] ?? '')) : '';
+            $g['results'][] = $res;
+            if (is_array($sc) && $res !== '') {
+                $aid = (int)($sc['admin_id'] ?? 0);
+                if ($aid > 0) {
+                    $g['score_admin_ids'][$aid] = true;
+                }
+                $st = trim((string)($sc['updatetime'] ?? ''));
+                if ($st === '') {
+                    $st = trim((string)($sc['createtime'] ?? ''));
+                }
+                if ($st !== '' && ($g['score_time_raw'] === '' || strcmp($st, $g['score_time_raw']) > 0)) {
+                    $g['score_time_raw'] = $st;
+                }
+            }
+            unset($g);
+        }
+
+        $out = [];
+        foreach ($groups as $ccydh => $g) {
+            $gymcList = is_array($g['CGYMC_list'] ?? null) ? $g['CGYMC_list'] : [];
+            $company = (string)($g['pick_company_name'] ?? '');
+            $cyjmc = (string)($g['CYJMC'] ?? '');
+            if ($keyword !== '') {
+                $blob = $ccydh . ' ' . $cyjmc . ' ' . implode('、', $gymcList) . ' ' . $company;
+                if (mb_stripos($blob, $keyword) === false) {
+                    continue;
+                }
+            }
+            $results = is_array($g['results'] ?? null) ? $g['results'] : [];
+            $uniq = [];
+            $allScored = true;
+            foreach ($results as $r) {
+                $r = trim((string)$r);
+                if ($r === '') {
+                    $allScored = false;
+                    continue;
+                }
+                $uniq[$r] = true;
+            }
+            $orderResult = '';
+            if ($allScored && count($uniq) === 1) {
+                $keys = array_keys($uniq);
+                $orderResult = (string)($keys[0] ?? '');
+            } elseif ($allScored && count($uniq) > 1) {
+                $orderResult = '';
+            }
+            if ($tab === 'pending' && $orderResult !== '') {
+                continue;
+            }
+            if ($tab === 'pass' && $orderResult !== '合格') {
+                continue;
+            }
+            if ($tab === 'fail' && $orderResult !== '不合格') {
+                continue;
+            }
+            // 合格/不合格:仅当前账号操作过的
+            if ($tab === 'pass' || $tab === 'fail') {
+                if ($adminId <= 0) {
+                    continue;
+                }
+                $adminIds = is_array($g['score_admin_ids'] ?? null) ? $g['score_admin_ids'] : [];
+                if (empty($adminIds[$adminId])) {
+                    continue;
+                }
+            }
+            $scoreTimeRaw = trim((string)($g['score_time_raw'] ?? ''));
+            $scoreTime = $scoreTimeRaw !== ''
+                ? \app\common\library\ProcuremenTime::formatDisplayDateTime($scoreTimeRaw)
+                : '';
+            $out[] = [
+                'CCYDH'             => $ccydh,
+                'scydgy_id'         => (int)($g['scydgy_id'] ?? 0),
+                'scydgy_ids'        => array_values(array_unique(array_map('intval', $g['scydgy_ids'] ?? []))),
+                'CYJMC'             => $cyjmc,
+                'CGYMC'             => $gymcList !== [] ? implode('、', $gymcList) : '',
+                'CCLBMMC'           => (string)($g['CCLBMMC'] ?? ''),
+                'pick_company_name' => $company,
+                'pick_time'         => (string)($g['pick_time'] ?? ''),
+                'score_time'        => $scoreTime !== '' ? $scoreTime : $scoreTimeRaw,
+                'score_time_raw'    => $scoreTimeRaw,
+                'process_count'     => (int)($g['process_count'] ?? 0),
+                'result'            => $orderResult,
+            ];
+        }
+
+        usort($out, static function ($a, $b) use ($tab) {
+            if ($tab === 'pass' || $tab === 'fail') {
+                $ta = (string)($a['score_time_raw'] ?? '');
+                $tb = (string)($b['score_time_raw'] ?? '');
+                if ($ta !== $tb) {
+                    return strcmp($tb, $ta);
+                }
+            }
+            $ta = (string)($a['pick_time'] ?? '');
+            $tb = (string)($b['pick_time'] ?? '');
+            if ($ta !== $tb) {
+                return strcmp($tb, $ta);
+            }
+
+            return strcmp((string)($a['CCYDH'] ?? ''), (string)($b['CCYDH'] ?? ''));
+        });
+
+        return $out;
+    }
 }

+ 488 - 0
application/index/view/index/inboundscore.html

@@ -0,0 +1,488 @@
+<!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>
+    <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 .result-tag {
+            display: inline-block; margin-top: 10px; padding: 4px 10px;
+            border-radius: 6px; font-size: 13px; font-weight: 600; color: #fff;
+        }
+        .card .result-tag.pass { background: #27ae60; }
+        .card .result-tag.fail { background: #e74c3c; }
+        .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-pass { background: #27ae60; }
+        .btn-fail { background: #e74c3c; }
+        .btn-pass:disabled, .btn-fail: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;
+        }
+        .confirm-mask.show { display: flex; }
+        .confirm-box {
+            width: 100%; max-width: 320px; background: #fff; border-radius: 12px;
+            padding: 18px 16px 14px; box-shadow: 0 8px 28px rgba(0,0,0,.18);
+        }
+        .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 14px; text-align: left;
+        }
+        .confirm-box .confirm-meta b { color: #333; font-weight: 600; }
+        .confirm-box .confirm-msg {
+            text-align: center; font-size: 15px; color: #333; line-height: 1.5;
+            margin: 0 0 14px; font-weight: 600;
+        }
+        .confirm-box .confirm-acts { display: flex; gap: 10px; }
+        .confirm-box .confirm-acts button {
+            flex: 1; padding: 10px 8px; border: none; border-radius: 8px;
+            font-size: 14px; font-weight: 600; font-family: inherit;
+        }
+        .confirm-cancel { background: #f0f0f0; color: #555; }
+        .confirm-ok { background: #3c8dbc; color: #fff; }
+        .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="pass">合格</button>
+            <button type="button" class="cat" data-list-tab="fail">不合格</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.CGYMC|default='—'|htmlentities}</div>
+                    <div><b>供应商:</b>{$row.pick_company_name|default='—'|htmlentities}</div>
+                </div>
+                <div class="actions">
+                    <button type="button" class="btn-pass" data-result="合格">合格</button>
+                    <button type="button" class="btn-fail" data-result="不合格">不合格</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.company_name|default='管理员'|htmlentities}</div>
+        <div class="me-row"><span>姓名</span>{$mprocProfile.contact_name|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="confirmMask">
+    <div class="confirm-box">
+        <div class="confirm-ord" id="confirmOrd"></div>
+        <div class="confirm-meta" id="confirmMeta"></div>
+        <p class="confirm-msg" id="confirmMsg">是否确认?</p>
+        <div class="confirm-acts">
+            <button type="button" class="confirm-cancel" id="confirmCancel">取消</button>
+            <button type="button" class="confirm-ok" id="confirmOk">确认</button>
+        </div>
+    </div>
+</div>
+<script>
+(function () {
+    var listUrl = "{:url('index/index/inboundscorelist')}";
+    var saveUrl = "{:url('index/index/inboundscoresave')}";
+    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) {}
+    }
+
+    // 退出前清掉本地 token,否则登录页会用 localStorage 自动恢复登录
+    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 === 'pass') return '暂无合格订单';
+        if (tab === 'fail') 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.CGYMC || '—') + '</div>';
+            html += '<div><b>供应商:</b>' + esc(row.pick_company_name || '—') + '</div>';
+            html += '</div>';
+            if (pending) {
+                html += '<div class="actions">'
+                    + '<button type="button" class="btn-pass" data-result="合格">合格</button>'
+                    + '<button type="button" class="btn-fail" data-result="不合格">不合格</button>'
+                    + '</div>';
+            } else {
+                var res = String(row.result || '');
+                var cls = res === '不合格' ? 'fail' : 'pass';
+                html += '<span class="result-tag ' + cls + '">' + esc(res || '—') + '</span>';
+                if (row.score_time) {
+                    html += '<div class="score-time">评定时间:' + esc(row.score_time) + '</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 confirmMask = document.getElementById('confirmMask');
+    var confirmOrd = document.getElementById('confirmOrd');
+    var confirmMeta = document.getElementById('confirmMeta');
+    var confirmMsg = document.getElementById('confirmMsg');
+    var confirmPending = null;
+
+    function textOf(el) {
+        return el ? String(el.textContent || '').replace(/\s+/g, ' ').trim() : '';
+    }
+
+    function askConfirm(card, result, onOk) {
+        confirmPending = onOk || 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 (confirmOrd) confirmOrd.textContent = ord;
+        if (confirmMeta) {
+            confirmMeta.innerHTML = '<div><b>工序:</b>' + esc(gymc) + '</div>'
+                + '<div><b>供应商:</b>' + esc(company) + '</div>';
+        }
+        if (confirmMsg) confirmMsg.textContent = '是否确认评定为「' + result + '」?';
+        if (confirmMask) confirmMask.classList.add('show');
+    }
+    function closeConfirm() {
+        confirmPending = null;
+        if (confirmMask) confirmMask.classList.remove('show');
+    }
+    document.getElementById('confirmCancel').addEventListener('click', closeConfirm);
+    document.getElementById('confirmOk').addEventListener('click', function () {
+        var fn = confirmPending;
+        closeConfirm();
+        if (typeof fn === 'function') fn();
+    });
+    confirmMask.addEventListener('click', function (e) {
+        if (e.target === confirmMask) closeConfirm();
+    });
+
+    function submitScore(card, result) {
+        var ccydh = card.getAttribute('data-ccydh');
+        if (!ccydh || !result) return;
+        var buttons = card.querySelectorAll('button');
+        buttons.forEach(function (b) { b.disabled = true; });
+        postForm(saveUrl, { ccydh: ccydh, result: result }).then(function (ret) {
+            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;
+            }
+            showToast(ret.msg || '已保存', 'ok');
+            card.parentNode && card.parentNode.removeChild(card);
+            if (!document.querySelector('#listMain .card')) {
+                document.getElementById('listMain').innerHTML = '<div class="empty">暂无待评分订单</div>';
+            }
+        }).catch(function () {
+            buttons.forEach(function (b) { b.disabled = false; });
+            showToast('网络错误', 'err');
+        });
+    }
+
+    document.getElementById('listMain').addEventListener('click', function (e) {
+        var btn = e.target.closest('button[data-result]');
+        if (!btn) return;
+        var card = btn.closest('.card');
+        if (!card) return;
+        var result = btn.getAttribute('data-result');
+        if (!result) return;
+        askConfirm(card, result, function () {
+            submitScore(card, result);
+        });
+    });
+})();
+</script>
+</body>
+</html>

+ 124 - 44
application/index/view/index/index.html

@@ -69,11 +69,25 @@
             align-self: stretch;
             background: #fff;
             border-right: 1px solid #e5e5e5;
-            padding: 8px 0;
+            padding: 4px 0 8px;
             position: relative;
             z-index: 1;
+            overflow-y: auto;
+            -webkit-overflow-scrolling: touch;
+        }
+        .mproc-side-group { margin: 0 0 4px; }
+        .mproc-side-group-title {
+            display: block;
+            padding: 10px 4px 2px;
+            font-size: 13px;
+            font-weight: 700;
+            color: #222;
+            text-align: center;
+            line-height: 1.3;
+            pointer-events: none;
+            user-select: none;
         }
-        .mproc-cat { display: block; width: 100%; padding: 12px 6px; border: none; background: transparent; font-size: 13px; color: #555; cursor: pointer; text-align: center; font-family: inherit; line-height: 1.35; -webkit-tap-highlight-color: transparent; }
+        .mproc-cat { display: block; width: 100%; padding: 9px 4px; border: none; background: transparent; font-size: 12px; color: #666; cursor: pointer; text-align: center; font-family: inherit; line-height: 1.35; -webkit-tap-highlight-color: transparent; }
         .mproc-cat.active { color: #3c8dbc; font-weight: 600; background: #f0f7fb; box-shadow: inset 3px 0 0 #3c8dbc; }
         .mproc-order-main {
             position: absolute;
@@ -694,23 +708,33 @@
     </div>
 </form>
 <div class="mproc-order-body">
-    <nav class="mproc-side" id="mproc-list-cats" aria-label="订单状态">
-        <button type="button" class="mproc-cat {eq name='mprocTab' value='draft'}active{/eq}" data-list-tab="draft">未提交</button>
-        <button type="button" class="mproc-cat {eq name='mprocTab' value='submitted'}active{/eq}" data-list-tab="submitted">已提交</button>
-        <button type="button" class="mproc-cat {eq name='mprocTab' value='done'}active{/eq}" data-list-tab="done">已完成</button>
+    <nav class="mproc-side" id="mproc-list-cats" aria-label="订单分类">
+        <div class="mproc-side-group">
+            <div class="mproc-side-group-title">询价</div>
+            <button type="button" class="mproc-cat {eq name='mprocTab' value='rfq'}active{/eq}" data-list-tab="rfq">待报价</button>
+            <button type="button" class="mproc-cat {eq name='mprocTab' value='rfq_submitted'}active{/eq}" data-list-tab="rfq_submitted">已报价</button>
+        </div>
+        <div class="mproc-side-group">
+            <div class="mproc-side-group-title">报价</div>
+            <button type="button" class="mproc-cat {eq name='mprocTab' value='draft'}active{/eq}" data-list-tab="draft">未提交</button>
+            <button type="button" class="mproc-cat {eq name='mprocTab' value='submitted'}active{/eq}" data-list-tab="submitted">已提交</button>
+            <button type="button" class="mproc-cat {eq name='mprocTab' value='done'}active{/eq}" data-list-tab="done">已完成</button>
+        </div>
     </nav>
     <div class="mproc-order-main">
     <div class="list" id="mproc-list-inner">
     {empty name="groups"}
     {notempty name="rows"}
     {volist name="rows" id="r"}
-    <div class="card order-group-card js-order-group{if $mprocFocusEid && $mprocFocusEid == $r.eid && ($mprocFocusTab == '' || $mprocFocusTab == $mprocTab)} mproc-card-highlight{/if}"{notempty name="r.mproc_remark"} data-remark="{$r.mproc_remark|htmlentities}"{/notempty} data-delivery-deadline="{$r.mproc_delivery_deadline|default=''|htmlentities}" data-bid-deadline="{$r.mproc_bid_deadline|default=''|htmlentities}" data-bid-open="{$r.mproc_bid_open_verified|default=0}">
+    <div class="card order-group-card js-order-group{if $mprocFocusEid && $mprocFocusEid == $r.eid && ($mprocFocusTab == '' || $mprocFocusTab == $mprocTab)} mproc-card-highlight{/if}"{notempty name="r.mproc_remark"} data-remark="{$r.mproc_remark|htmlentities}"{/notempty} data-delivery-deadline="{eq name='mprocIsRfqTab' value='1'}{else /}{$r.mproc_delivery_deadline|default=''|htmlentities}{/eq}" data-bid-deadline="{eq name='mprocIsRfqTab' value='1'}{else /}{$r.mproc_bid_deadline|default=''|htmlentities}{/eq}" data-bid-open="{eq name='mprocIsRfqTab' value='1'}0{else /}{$r.mproc_bid_open_verified|default=0}{/eq}" data-is-rfq="{$mprocIsRfqTab|default=0}">
         <div class="order-group-head">
-            <p class="title">{notempty name="r.CCYDH"}<span class="mproc-ord-no">{$r.CCYDH}</span>{/notempty}{$r.CYJMC|default=''}{eq name="mprocTab" value="draft"}<em class="mproc-pick-badge mproc-pick-unquoted">未报价</em>{/eq}{eq name="mprocTab" value="submitted"}<em class="mproc-pick-badge mproc-pick-quoted">已报价</em>{/eq}{notempty name="r.mproc_done_label"}{eq name="mprocTab" value="done"}<em class="mproc-pick-badge {eq name='r.mproc_pick_result' value='中标'}mproc-pick-win{else /}{eq name='r.mproc_pick_result' value='未中标'}mproc-pick-lose{else /}mproc-pick-expired{/eq}{/eq}">{$r.mproc_done_label|htmlentities}</em>{/eq}{/notempty}</p>
+            <p class="title">{notempty name="r.CCYDH"}<span class="mproc-ord-no">{$r.CCYDH}</span>{/notempty}{$r.CYJMC|default=''}{eq name="mprocStatusTab" value="draft"}{eq name="mprocIsRfqTab" value="1"}<em class="mproc-pick-badge mproc-pick-unquoted">待报价</em>{else /}<em class="mproc-pick-badge mproc-pick-unquoted">未报价</em>{/eq}{/eq}{eq name="mprocStatusTab" value="submitted"}<em class="mproc-pick-badge mproc-pick-quoted">已报价</em>{/eq}{notempty name="r.mproc_done_label"}{eq name="mprocStatusTab" value="done"}<em class="mproc-pick-badge {eq name='r.mproc_pick_result' value='中标'}mproc-pick-win{else /}{eq name='r.mproc_pick_result' value='未中标'}mproc-pick-lose{else /}mproc-pick-expired{/eq}{/eq}">{$r.mproc_done_label|htmlentities}</em>{/eq}{/notempty}</p>
+            {eq name="mprocIsRfqTab" value="0"}
             <div class="kv-row order-group-deadline-row">
                 <div class="kv"><span>招标截止日期</span>{notempty name="r.mproc_bid_deadline_display"}{$r.mproc_bid_deadline_display|htmlentities}{else /}—{/notempty}</div>
                 <div class="kv"><span>交货截止日期</span>{notempty name="r.mproc_delivery_deadline_display"}{$r.mproc_delivery_deadline_display|htmlentities}{else /}—{/notempty}</div>
             </div>
+            {/eq}
         </div>
         <div class="order-line js-line"
              data-id="{$r.eid|default=0}"
@@ -733,13 +757,15 @@
         {notempty name="r.mproc_remark"}
         <div class="kv order-group-remark"><span>备注</span>{$r.mproc_remark|htmlentities}</div>
         {/notempty}
+        {eq name="mprocIsRfqTab" value="0"}
         <div class="kv-row order-group-lead-row">
             <div class="kv"><span>工期</span>{if $r.lead_days_missing}<span class="kv-miss">未填写</span>{else /}{$r.lead_days_display|htmlentities}天{/if}</div>
             <div class="kv"><span>交货日期</span>{if $r.delivery_missing}<span class="kv-miss">未填写</span>{else /}{$r.delivery_display|htmlentities}{/if}</div>
         </div>
+        {/eq}
         {eq name="r.mproc_can_edit" value="1"}
         <div class="order-group-foot">
-            <button type="button" class="btn-edit js-open-edit" data-stop="1">填写单价/工期</button>
+            <button type="button" class="btn-edit js-open-edit" data-stop="1">{eq name="mprocIsRfqTab" value="1"}填写单价{else /}填写单价/工期{/eq}</button>
         </div>
         {/eq}
     </div>
@@ -749,13 +775,15 @@
     {/notempty}
     {else /}
     {volist name="groups" id="g"}
-    <div class="card order-group-card js-order-group{volist name='g.lines' id='_fe'}{if $mprocFocusEid && $mprocFocusEid == $_fe.eid && ($mprocFocusTab == '' || $mprocFocusTab == $mprocTab)} mproc-card-highlight{/if}{/volist}"{notempty name="g.remark"} data-remark="{$g.remark|htmlentities}"{/notempty} data-delivery-deadline="{$g.mproc_delivery_deadline|default=''|htmlentities}" data-bid-deadline="{$g.mproc_bid_deadline|default=''|htmlentities}" data-bid-open="{$g.mproc_bid_open_verified|default=0}">
+    <div class="card order-group-card js-order-group{volist name='g.lines' id='_fe'}{if $mprocFocusEid && $mprocFocusEid == $_fe.eid && ($mprocFocusTab == '' || $mprocFocusTab == $mprocTab)} mproc-card-highlight{/if}{/volist}"{notempty name="g.remark"} data-remark="{$g.remark|htmlentities}"{/notempty} data-delivery-deadline="{eq name='mprocIsRfqTab' value='1'}{else /}{$g.mproc_delivery_deadline|default=''|htmlentities}{/eq}" data-bid-deadline="{eq name='mprocIsRfqTab' value='1'}{else /}{$g.mproc_bid_deadline|default=''|htmlentities}{/eq}" data-bid-open="{eq name='mprocIsRfqTab' value='1'}0{else /}{$g.mproc_bid_open_verified|default=0}{/eq}" data-is-rfq="{$mprocIsRfqTab|default=0}">
         <div class="order-group-head">
-            <p class="title">{notempty name="g.CCYDH"}<span class="mproc-ord-no">{$g.CCYDH}</span>{/notempty}{$g.CYJMC|default=''}{eq name="mprocTab" value="draft"}<em class="mproc-pick-badge mproc-pick-unquoted">未报价</em>{/eq}{eq name="mprocTab" value="submitted"}<em class="mproc-pick-badge mproc-pick-quoted">已报价</em>{/eq}{notempty name="g.mproc_done_label"}{eq name="mprocTab" value="done"}<em class="mproc-pick-badge {eq name='g.mproc_pick_result' value='中标'}mproc-pick-win{else /}{eq name='g.mproc_pick_result' value='未中标'}mproc-pick-lose{else /}mproc-pick-expired{/eq}{/eq}">{$g.mproc_done_label|htmlentities}</em>{/eq}{/notempty}</p>
+            <p class="title">{notempty name="g.CCYDH"}<span class="mproc-ord-no">{$g.CCYDH}</span>{/notempty}{$g.CYJMC|default=''}{eq name="mprocStatusTab" value="draft"}{eq name="mprocIsRfqTab" value="1"}<em class="mproc-pick-badge mproc-pick-unquoted">待报价</em>{else /}<em class="mproc-pick-badge mproc-pick-unquoted">未报价</em>{/eq}{/eq}{eq name="mprocStatusTab" value="submitted"}<em class="mproc-pick-badge mproc-pick-quoted">已报价</em>{/eq}{notempty name="g.mproc_done_label"}{eq name="mprocStatusTab" value="done"}<em class="mproc-pick-badge {eq name='g.mproc_pick_result' value='中标'}mproc-pick-win{else /}{eq name='g.mproc_pick_result' value='未中标'}mproc-pick-lose{else /}mproc-pick-expired{/eq}{/eq}">{$g.mproc_done_label|htmlentities}</em>{/eq}{/notempty}</p>
+            {eq name="mprocIsRfqTab" value="0"}
             <div class="kv-row order-group-deadline-row">
                 <div class="kv"><span>招标截止日期</span>{notempty name="g.mproc_bid_deadline_display"}{$g.mproc_bid_deadline_display|htmlentities}{else /}—{/notempty}</div>
                 <div class="kv"><span>交货截止日期</span>{notempty name="g.mproc_delivery_deadline_display"}{$g.mproc_delivery_deadline_display|htmlentities}{else /}—{/notempty}</div>
             </div>
+            {/eq}
         </div>
         {volist name="g.lines" id="r"}
         <div class="order-line js-line"
@@ -781,6 +809,7 @@
         <div class="kv order-group-remark"><span>备注</span>{$g.remark|htmlentities}</div>
         {/notempty}
         {notempty name="g.lines"}
+        {eq name="mprocIsRfqTab" value="0"}
         <?php
             $__leadMiss = 1; $__leadDisp = ''; $__delMiss = 1; $__delDisp = '';
             foreach ((array)$g['lines'] as $__lr) {
@@ -797,10 +826,11 @@
             <div class="kv"><span>工期</span>{if condition="$__leadMiss"}<span class="kv-miss">未填写</span>{else /}{$__leadDisp|htmlentities}天{/if}</div>
             <div class="kv"><span>交货日期</span>{if condition="$__delMiss"}<span class="kv-miss">未填写</span>{else /}{$__delDisp|htmlentities}{/if}</div>
         </div>
+        {/eq}
         {/notempty}
         {eq name="g.can_edit" value="1"}
         <div class="order-group-foot">
-            <button type="button" class="btn-edit js-open-edit" data-stop="1">填写单价/工期</button>
+            <button type="button" class="btn-edit js-open-edit" data-stop="1">{eq name="mprocIsRfqTab" value="1"}填写单价{else /}填写单价/工期{/eq}</button>
         </div>
         {/eq}
     </div>
@@ -993,7 +1023,34 @@
     var boot = {:json_encode(['main_tab' => $mprocMainTab, 'tab' => $mprocTab, 'q' => $mprocSearchQ, 'is_admin' => $mprocIsAdmin, 'can_change_pwd' => isset($mprocCanChangePwd) ? (int)$mprocCanChangePwd : 0, 'focus_eid' => isset($mprocFocusEid) ? (int)$mprocFocusEid : 0, 'focus_tab' => isset($mprocFocusTab) ? (string)$mprocFocusTab : ''], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)};
     var changePwdUrl = mprocEndpointUrl('mprocChangePwd.html');
     var currentMain = boot.main_tab === 'me' ? 'me' : 'orders';
-    var currentListTab = boot.tab && ['draft', 'submitted', 'done'].indexOf(boot.tab) !== -1 ? boot.tab : 'draft';
+    var MPROC_LIST_TABS = ['rfq', 'rfq_submitted', 'draft', 'submitted', 'done'];
+    var currentListTab = (function () {
+        var t = boot.tab || 'draft';
+        if (t === 'rfq_done') {
+            t = 'rfq_submitted';
+        }
+        return MPROC_LIST_TABS.indexOf(t) !== -1 ? t : 'draft';
+    })();
+    var currentEditIsRfq = false;
+
+    function mprocIsRfqListTab(tab) {
+        tab = tab || currentListTab;
+        return tab === 'rfq' || tab === 'rfq_submitted';
+    }
+
+    function mprocStatusFromListTab(tab) {
+        tab = tab || currentListTab;
+        if (tab === 'rfq' || tab === 'draft') {
+            return 'draft';
+        }
+        if (tab === 'rfq_submitted' || tab === 'submitted') {
+            return 'submitted';
+        }
+        if (tab === 'done') {
+            return 'done';
+        }
+        return 'draft';
+    }
 
     /** 短信/邮件链接中的 focus_eid:boot / URL / hash / sessionStorage(邮箱客户端常丢 query) */
     function mprocPersistFocusEid(eid) {
@@ -1344,17 +1401,21 @@
     }
 
     function mprocResolveGroupStatusBadge(g, lines) {
-        if (currentListTab === 'draft') {
+        var st = mprocStatusFromListTab(currentListTab);
+        if (st === 'draft') {
+            if (mprocIsRfqListTab(currentListTab)) {
+                return '<em class="mproc-pick-badge mproc-pick-unquoted">待报价</em>';
+            }
             return '<em class="mproc-pick-badge mproc-pick-unquoted">未报价</em>';
         }
-        if (currentListTab === 'submitted') {
+        if (st === 'submitted') {
             return '<em class="mproc-pick-badge mproc-pick-quoted">已报价</em>';
         }
         return mprocResolveGroupDoneBadge(g, lines);
     }
 
     function mprocPickBadgeHtml(label, pick) {
-        if (currentListTab !== 'done') {
+        if (mprocStatusFromListTab(currentListTab) !== 'done') {
             return '';
         }
         label = String(label || '').trim();
@@ -1434,6 +1495,9 @@
     }
 
     function mprocPickGroupLeadDeliveryHtml(lines) {
+        if (mprocIsRfqListTab(currentListTab)) {
+            return '';
+        }
         var leadHtml = '<span class="kv-miss">未填写</span>';
         var delHtml = '<span class="kv-miss">未填写</span>';
         var gotLead = false;
@@ -1515,10 +1579,11 @@
         var remarkHtml = remark
             ? ('<div class="kv order-group-remark"><span>备注</span>' + mprocEsc(remark) + '</div>')
             : '';
-        var deadlineHtml = '<div class="kv-row order-group-deadline-row">'
+        var isRfq = mprocIsRfqListTab(currentListTab);
+        var deadlineHtml = isRfq ? '' : ('<div class="kv-row order-group-deadline-row">'
             + '<div class="kv"><span>招标截止日期</span>' + mprocEsc(bidDl || '—') + '</div>'
             + '<div class="kv"><span>交货截止日期</span>' + mprocEsc(delDlDisp || '—') + '</div>'
-            + '</div>';
+            + '</div>');
         var badgeHtml = mprocResolveGroupStatusBadge(g, lines);
         var shouldFocus = focusEid > 0 && (focusTab === '' || focusTab === currentListTab);
         var groupHl = shouldFocus && lines.some(function (r) {
@@ -1528,9 +1593,10 @@
             return renderOrderLineHtml(r);
         }).join('');
         return '<div class="card order-group-card js-order-group' + groupHl + '"' + remarkAttr
-            + ' data-delivery-deadline="' + mprocEscAttr(delDl || '') + '"'
-            + ' data-bid-deadline="' + mprocEscAttr(bidDlRaw || '') + '"'
-            + ' data-bid-open="' + (bidOpen ? '1' : '0') + '">'
+            + ' data-delivery-deadline="' + mprocEscAttr(isRfq ? '' : (delDl || '')) + '"'
+            + ' data-bid-deadline="' + mprocEscAttr(isRfq ? '' : (bidDlRaw || '')) + '"'
+            + ' data-bid-open="' + (isRfq ? '0' : (bidOpen ? '1' : '0')) + '"'
+            + ' data-is-rfq="' + (isRfq ? '1' : '0') + '">'
             + '<div class="order-group-head">'
             + '<p class="title">' + (ccydh ? ('<span class="mproc-ord-no">' + mprocEsc(ccydh) + '</span>') : '') + mprocEsc(tit) + badgeHtml + '</p>'
             + deadlineHtml
@@ -1539,7 +1605,9 @@
             + remarkHtml
             + mprocPickGroupLeadDeliveryHtml(lines)
             + (canEdit
-                ? '<div class="order-group-foot"><button type="button" class="btn-edit js-open-edit" data-stop="1">填写单价/工期</button></div>'
+                ? '<div class="order-group-foot"><button type="button" class="btn-edit js-open-edit" data-stop="1">'
+                    + (mprocIsRfqListTab(currentListTab) ? '填写单价' : '填写单价/工期')
+                    + '</button></div>'
                 : '')
             + '</div>';
     }
@@ -1554,7 +1622,7 @@
         }
         if (!rows || !rows.length) {
             if (doneNoStatus) {
-                return '<div class="empty">表中未找到 status_name 列,无法按「未提交/已提交/已完成」筛选;请在 purchase_order_detail 增加该字段后由后端写入状态文案</div>';
+                return '<div class="empty">表中未找到 status_name 列,无法按「询价/报价/已提交/已完成」筛选;请在 purchase_order_detail 增加该字段后由后端写入状态文案</div>';
             }
             return '<div class="empty">暂无数据</div>';
         }
@@ -1577,11 +1645,12 @@
         return inp ? String(inp.value || '').trim() : '';
     }
 
-    /** 拉取订单列表:左侧 status 分类 draft=0 / submitted=1 / done=2 */
+    /** 拉取订单列表:左侧分类 rfq=询价 / draft=报价 / submitted / done */
     function fetchOrderList(listTab, q, opts) {
         opts = opts || {};
         var clearFocus = !!opts.clearFocus;
-        var nextTab = listTab && ['draft', 'submitted', 'done'].indexOf(listTab) !== -1 ? listTab : 'draft';
+        var nextTab = listTab === 'rfq_done' ? 'rfq_submitted' : listTab;
+        nextTab = nextTab && MPROC_LIST_TABS.indexOf(nextTab) !== -1 ? nextTab : 'draft';
         if (clearFocus || (mprocGetFocusEid() > 0 && boot.focus_tab && boot.focus_tab !== nextTab)) {
             mprocClearFocusEid();
             clearFocus = true;
@@ -2279,6 +2348,9 @@
     }
 
     function buildEditOrderSharedHtml(lines) {
+        if (currentEditIsRfq || mprocIsRfqListTab(currentListTab)) {
+            return '';
+        }
         var seed = lines && lines.length ? lines[0] : {};
         (lines || []).forEach(function (line) {
             if ((!seed.lead_days || String(seed.lead_days).trim() === '') && line.lead_days) {
@@ -2352,16 +2424,18 @@
         if (!lines.length) {
             return;
         }
+        var isRfqCard = groupEl && String(groupEl.getAttribute('data-is-rfq') || '') === '1';
+        currentEditIsRfq = isRfqCard || mprocIsRfqListTab(currentListTab);
         var bidRaw = groupEl ? String(groupEl.getAttribute('data-bid-deadline') || '').trim() : '';
         var bidOpen = groupEl ? String(groupEl.getAttribute('data-bid-open') || '') === '1' : false;
-        if (bidOpen) {
+        if (!currentEditIsRfq && bidOpen) {
             mprocShowToast('已开标验证,不可再提交');
             if (currentMain === 'orders') {
                 fetchOrderList(currentListTab, getSearchQ());
             }
             return;
         }
-        if (mprocIsBidDeadlineReached(bidRaw)) {
+        if (!currentEditIsRfq && mprocIsBidDeadlineReached(bidRaw)) {
             mprocShowToast('已超过招标截止时间,不可再提交');
             if (currentMain === 'orders') {
                 fetchOrderList(currentListTab, getSearchQ());
@@ -2369,18 +2443,18 @@
             return;
         }
         currentEditLines = lines;
-        currentEditBidDeadline = bidRaw;
+        currentEditBidDeadline = currentEditIsRfq ? '' : bidRaw;
         currentEditBidOpen = false;
         var dlRaw = groupEl ? String(groupEl.getAttribute('data-delivery-deadline') || '').trim() : '';
         currentEditDeliveryDeadline = (dlRaw && /^\d{4}-\d{2}-\d{2}/.test(dlRaw)) ? dlRaw.slice(0, 10) : '';
         var first = lines[0];
         var head = [first.ccydh, first.title].filter(function (s) { return s && s.length; }).join(' ');
         if (titleEl) {
-            titleEl.textContent = head || '填写单价/工期';
+            titleEl.textContent = head || (currentEditIsRfq ? '填写单价' : '填写单价/工期');
         }
         if (editLinesBox) {
             var groupRemark = String(groupEl.getAttribute('data-remark') || '').replace(/&quot;/g, '"');
-            // 工序单价 → 工期/交期(同一行)→ 备注在最下
+            // 工序单价 → 工期/交期(同一行,询价跳过)→ 备注在最下
             editLinesBox.innerHTML = lines.map(function (line, idx) {
                 return buildEditLineBlockHtml(line, idx, lines.length);
             }).join('') + buildEditOrderSharedHtml(lines) + buildEditRemarkHtml(groupRemark);
@@ -2410,6 +2484,7 @@
         mask.classList.remove('show');
         mask.setAttribute('aria-hidden', 'true');
         currentEditLines = [];
+        currentEditIsRfq = false;
         currentEditDeliveryDeadline = '';
         currentEditBidDeadline = '';
         currentEditBidOpen = false;
@@ -2493,21 +2568,23 @@
             mprocShowToast('请至少填写一道工序的单价');
             return null;
         }
-        if (sharedLead === '' || sharedDelivery === '') {
-            mprocShowToast('请填写整单工期与交期');
-            if (sharedLead === '' && leadInp) {
-                leadInp.focus();
-            } else if (delInp) {
-                delInp.focus();
+        if (!currentEditIsRfq && !mprocIsRfqListTab(currentListTab)) {
+            if (sharedLead === '' || sharedDelivery === '') {
+                mprocShowToast('请填写整单工期与交期');
+                if (sharedLead === '' && leadInp) {
+                    leadInp.focus();
+                } else if (delInp) {
+                    delInp.focus();
+                }
+                return null;
             }
-            return null;
-        }
-        if (!/^\d+$/.test(sharedLead)) {
-            mprocShowToast('工期须为非负整数(天)');
-            if (leadInp) {
-                leadInp.focus();
+            if (!/^\d+$/.test(sharedLead)) {
+                mprocShowToast('工期须为非负整数(天)');
+                if (leadInp) {
+                    leadInp.focus();
+                }
+                return null;
             }
-            return null;
         }
         // 提交前再判一次,防止填表过程中刚好截止
         if (!mprocAssertEditBidDeadlineOk()) {
@@ -2693,7 +2770,10 @@
                 try {
                     saved = localStorage.getItem('mproc_list_tab') || '';
                 } catch (ignoreStore) {}
-                if (saved && ['draft', 'submitted', 'done'].indexOf(saved) !== -1 && saved !== currentListTab) {
+                if (saved === 'rfq_done') {
+                    saved = 'rfq_submitted';
+                }
+                if (saved && MPROC_LIST_TABS.indexOf(saved) !== -1 && saved !== currentListTab) {
                     fetchOrderList(saved, getSearchQ());
                     return;
                 }

Разница между файлами не показана из-за своего большого размера
+ 963 - 63
public/assets/js/backend/procuremen.js


+ 21 - 1
public/assets/js/backend/procuremenarchive.js

@@ -92,7 +92,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                         },
                         {
                             field: 'pick_company_name',
-                            title: '已选供应商',
+                            title: '中标供应商',
                             operate: 'LIKE',
                             width: 180,
                             class: 'autocontent',
@@ -100,6 +100,26 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                                 return archiveCellText(value);
                             }
                         },
+                        {
+                            field: 'inbound_result',
+                            title: '是否合格',
+                            operate: false,
+                            width: 100,
+                            align: 'center',
+                            formatter: function (value) {
+                                var v = value == null ? '' : String(value);
+                                if (v === '合格') {
+                                    return '<span class="label label-success">合格</span>';
+                                }
+                                if (v === '不合格') {
+                                    return '<span class="label label-danger">不合格</span>';
+                                }
+                                if (v.indexOf('合格') >= 0 || v.indexOf('不合格') >= 0) {
+                                    return archiveCellText(v);
+                                }
+                                return v ? archiveCellText(v) : '';
+                            }
+                        },
                         {
                             field: 'createtime',
                             title: '完结时间',

+ 150 - 78
public/assets/js/backend/procuremensms.js

@@ -13,95 +13,165 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
             });
 
             var table = $("#table");
-            var colCenter = {align: 'center', valign: 'middle'};
+            var colCenter = {align: 'center', halign: 'center', valign: 'middle'};
+
+            var columns = [
+                {checkbox: true, align: 'center', halign: 'center', valign: 'middle', width: 42},
+                $.extend({field: 'id', title: '序号', width: 60, operate: false}, colCenter),
+                $.extend({field: 'title', title: '模版名称', operate: 'LIKE', width: 160}, colCenter),
+                $.extend({
+                    field: 'content',
+                    title: '模版正文',
+                    operate: 'LIKE',
+                    width: 420,
+                    align: 'left',
+                    halign: 'center',
+                    valign: 'middle',
+                    class: 'col-sms-content',
+                    formatter: function (value) {
+                        if (value == null || value === '') {
+                            return '';
+                        }
+                        return '<div class="sms-content-cell">' + $('<div/>').text(String(value)).html() + '</div>';
+                    }
+                }, {}),
+                $.extend({
+                    field: 'status',
+                    title: __('Status'),
+                    width: 90,
+                    operate: '=',
+                    searchList: {'1': '正常', '0': '禁用'},
+                    formatter: function (value) {
+                        var v = String(value);
+                        if (v === '1' || v === 'normal') {
+                            return '<span class="text-success"><i class="fa fa-circle"></i> 正常</span>';
+                        }
+                        return '<span class="text-muted"><i class="fa fa-circle"></i> 禁用</span>';
+                    }
+                }, colCenter),
+                $.extend({
+                    field: 'updatetime',
+                    title: __('Updatetime'),
+                    width: 170,
+                    operate: 'RANGE',
+                    addclass: 'datetimerange',
+                    autocomplete: false,
+                    formatter: function (value) {
+                        if (value == null || value === '') {
+                            return '';
+                        }
+                        var s = String(value);
+                        if (/^\d{4}-\d{2}-\d{2}/.test(s)) {
+                            return s.length >= 19 ? s.substr(0, 19) : s;
+                        }
+                        return Table.api.formatter.datetime.call(this, value);
+                    }
+                }, colCenter)
+            ];
+
+            function smsGetColWidths() {
+                var widths = [];
+                $.each(columns, function (_, col) {
+                    if (!col || col.visible === false) {
+                        return;
+                    }
+                    var w = parseInt(col.width, 10);
+                    widths.push((!w || isNaN(w)) ? (col.checkbox ? 42 : 120) : w);
+                });
+                return widths;
+            }
+
+            function smsApplyColgroup($tbl, widths) {
+                if (!$tbl || !$tbl.length || !widths.length) {
+                    return;
+                }
+                var total = 0;
+                var i;
+                for (i = 0; i < widths.length; i++) {
+                    total += widths[i];
+                }
+                $tbl.css({
+                    tableLayout: 'fixed',
+                    width: total + 'px',
+                    minWidth: '100%'
+                });
+                var $cg = $tbl.children('colgroup');
+                if (!$cg.length) {
+                    $cg = $('<colgroup/>').prependTo($tbl);
+                }
+                $cg.empty();
+                for (i = 0; i < widths.length; i++) {
+                    $cg.append($('<col/>').attr('style', 'width:' + widths[i] + 'px;min-width:' + widths[i] + 'px;'));
+                }
+                $tbl.find('thead tr:first > th').each(function (idx) {
+                    if (idx >= widths.length) {
+                        return;
+                    }
+                    var w = widths[idx];
+                    $(this).css({width: w + 'px', minWidth: w + 'px', maxWidth: w + 'px'});
+                    $(this).find('.fht-cell').css({width: w + 'px', minWidth: w + 'px', maxWidth: w + 'px'});
+                });
+                $tbl.find('tbody tr:first-child:not(.no-records-found) > *').each(function (idx) {
+                    if (idx >= widths.length) {
+                        return;
+                    }
+                    var w = widths[idx];
+                    $(this).css({width: w + 'px', minWidth: w + 'px', maxWidth: w + 'px'});
+                });
+            }
+
+            function smsSyncHeaderBodyAlign() {
+                var $bt = table.closest('.bootstrap-table');
+                if (!$bt.length) {
+                    return;
+                }
+                var widths = smsGetColWidths();
+                if (!widths.length) {
+                    return;
+                }
+                var $headerWrap = $bt.find('> .fixed-table-container > .fixed-table-header').first();
+                var $bodyWrap = $bt.find('> .fixed-table-container > .fixed-table-body').first();
+                var $headerTable = $headerWrap.length ? $headerWrap.children('table').first() : $();
+                var $bodyTable = $bodyWrap.length ? $bodyWrap.children('table').first() : table;
+                if (!$bodyTable.length) {
+                    $bodyTable = $bt.find('.fixed-table-body > table').first();
+                }
+                if ($headerTable.length && $bodyTable.length && $headerTable[0] !== $bodyTable[0]) {
+                    var bodyEl = $bodyWrap[0];
+                    var sb = bodyEl ? Math.max(0, bodyEl.offsetWidth - bodyEl.clientWidth) : 0;
+                    $headerWrap.css('margin-right', sb > 0 ? sb + 'px' : '');
+                    smsApplyColgroup($headerTable, widths);
+                    smsApplyColgroup($bodyTable, widths);
+                    $headerWrap.scrollLeft($bodyWrap.scrollLeft() || 0);
+                } else if ($bodyTable.length) {
+                    smsApplyColgroup($bodyTable, widths);
+                }
+                $bt.find('thead th .th-inner').css({
+                    textAlign: 'center',
+                    justifyContent: 'center'
+                });
+            }
 
             table.bootstrapTable({
                 url: $.fn.bootstrapTable.defaults.extend.index_url,
                 pk: 'id',
                 sortName: 'id',
+                sortOrder: 'desc',
                 singleSelect: true,
                 clickToSelect: true,
                 commonSearch: true,
-                columns: [
-                    [
-                        {checkbox: true, align: 'center', valign: 'middle'},
-                        $.extend({field: 'id', title: __('Id'), width: 52, operate: 'LIKE'}, colCenter),
-                        $.extend({field: 'title', title: '模版名称', operate: 'LIKE', width: 160}, colCenter),
-                        $.extend({
-                            field: 'content',
-                            title: '模版正文',
-                            operate: 'LIKE',
-                            class: 'col-sms-content',
-                            width: 360,
-                            cellStyle: function () {
-                                return {css: {whiteSpace: 'pre-wrap', wordBreak: 'break-word', textAlign: 'center'}};
-                            },
-                            formatter: function (value) {
-                                if (value == null || value === '') {
-                                    return '';
-                                }
-                                return '<div class="sms-content-cell">' + $('<div/>').text(String(value)).html() + '</div>';
-                            }
-                        }, colCenter),
-                        $.extend({field: 'remark', title: '备注', operate: 'LIKE', width: 100}, colCenter),
-                        $.extend({
-                            field: 'status',
-                            title: __('Status'),
-                            width: 88,
-                            operate: '=',
-                            searchList: {'1': '正常', '0': '禁用'},
-                            formatter: function (value) {
-                                var v = String(value);
-                                if (v === '1' || v === 'normal') {
-                                    return '<span class="text-success"><i class="fa fa-circle"></i> 正常</span>';
-                                }
-                                return '<span class="text-muted"><i class="fa fa-circle"></i> 禁用</span>';
-                            },
-                            cellStyle: function () {
-                                return {css: {whiteSpace: 'nowrap', textAlign: 'center'}};
-                            }
-                        }, colCenter),
-                        $.extend({
-                            field: 'updatetime',
-                            title: __('Updatetime'),
-                            width: 158,
-                            operate: 'RANGE',
-                            addclass: 'datetimerange',
-                            autocomplete: false,
-                            formatter: function (value) {
-                                if (value == null || value === '') {
-                                    return '';
-                                }
-                                var s = String(value);
-                                if (/^\d{4}-\d{2}-\d{2}/.test(s)) {
-                                    return s.length >= 19 ? s.substr(0, 19) : s;
-                                }
-                                return Table.api.formatter.datetime.call(this, value);
-                            },
-                            cellStyle: function () {
-                                return {css: {whiteSpace: 'nowrap', textAlign: 'center'}};
-                            }
-                        }, colCenter)
-                        // $.extend({field: 'operate', title: __('Operate'), width: 72, table: table, events: Table.api.events.operate, formatter: Table.api.formatter.operate}, colCenter)
-                    ]
-                ]
+                showToggle: false,
+                showColumns: false,
+                escape: false,
+                columns: [columns]
             });
 
             Table.api.bindevent(table);
 
-            function procuremenSmsCenterTableHeader() {
-                var $wrap = table.closest('.bootstrap-table');
-                $wrap.find('.fixed-table-header thead th, .fixed-table-container thead th').css({
-                    'text-align': 'center',
-                    'vertical-align': 'middle'
-                });
-                $wrap.find('.fixed-table-header thead th .th-inner, .fixed-table-container thead th .th-inner').css({
-                    'text-align': 'center',
-                    'justify-content': 'center'
-                });
-            }
-
-            table.on('post-header.bs.table post-body.bs.table refresh.bs.table', procuremenSmsCenterTableHeader);
+            table.on('post-header.bs.table post-body.bs.table load-success.bs.table refresh.bs.table', function () {
+                setTimeout(smsSyncHeaderBodyAlign, 0);
+                setTimeout(smsSyncHeaderBodyAlign, 80);
+            });
 
             table.on('check.bs.table', function (e, row) {
                 var sel = table.bootstrapTable('getSelections') || [];
@@ -116,12 +186,14 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
             });
 
             table.on('post-body.bs.table', function () {
-                procuremenSmsCenterTableHeader();
                 table.closest('.bootstrap-table').find('.btn-editone').each(function () {
                     $(this).data('area', SMS_EDIT_AREA);
                 });
             });
             $('#toolbar .btn-edit').data('area', SMS_EDIT_AREA);
+            $(window).off('resize.procuremenSms').on('resize.procuremenSms', function () {
+                setTimeout(smsSyncHeaderBodyAlign, 50);
+            });
         },
         edit: function () {
             Controller.api.bindevent();

+ 11 - 7
public/assets/js/backend/supplierscorerule.js

@@ -13,7 +13,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
             });
             var table = $('#table');
             // 行内编辑弹窗尺寸
-            Table.button.edit.extend = 'data-toggle="tooltip" data-container="body" data-area=\'["460px","360px"]\'';
+            Table.button.edit.extend = 'data-toggle="tooltip" data-container="body" data-area=\'["520px","440px"]\'';
             table.bootstrapTable({
                 url: $.fn.bootstrapTable.defaults.extend.index_url,
                 pk: 'id',
@@ -22,7 +22,11 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                 columns: [[
                     {checkbox: true},
                     {field: 'id', title: 'ID', width: 60, sortable: true},
-                    {field: 'quality_weight', title: '商务/技术分%', operate: 'BETWEEN', sortable: true, formatter: function (value) {
+                    {field: 'name', title: '规则名称(订单类型)', operate: 'LIKE', sortable: true, formatter: function (value) {
+                        var t = value == null ? '' : String(value).trim();
+                        return t !== '' ? t : '-';
+                    }},
+                    {field: 'quality_weight', title: '质量分%', operate: 'BETWEEN', sortable: true, formatter: function (value) {
                         if (value === null || value === undefined || value === '') {
                             return '';
                         }
@@ -34,7 +38,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                         }
                         return String(parseInt(value, 10));
                     }},
-                    {field: 'lead_weight', title: '交分%', operate: 'BETWEEN', sortable: true, formatter: function (value) {
+                    {field: 'lead_weight', title: '交分%', operate: 'BETWEEN', sortable: true, formatter: function (value) {
                         if (value === null || value === undefined || value === '') {
                             return '0';
                         }
@@ -42,12 +46,12 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                     }},
                     {
                         field: 'is_default',
-                        title: '状态',
-                        searchList: {1: '使用', 0: '未使用'},
+                        title: '是否默认',
+                        searchList: {1: '启用', 0: '未启用'},
                         formatter: function (v) {
                             return parseInt(v, 10) === 1
-                                ? '<span class="label label-success">使用</span>'
-                                : '<span class="label label-default">未使用</span>';
+                                ? '<span class="label label-success">用</span>'
+                                : '<span class="label label-default">未用</span>';
                         }
                     },
                     {

+ 205 - 15
public/assets/js/backend/supplierservicescore.js

@@ -40,13 +40,12 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                     .replace(/</g, '&lt;')
                     .replace(/>/g, '&gt;');
             }
-            function scoreInputHtml(company, ym, cssClass, val, title) {
+            function scoreInputHtml(company, ym, cssClass, val) {
                 var show = (val == null || val === '') ? '' : String(val);
                 return '<input type="text" inputmode="decimal" class="form-control input-sm ' + cssClass + '"'
                     + ' data-company="' + escAttr(company) + '"'
                     + ' data-ym="' + escAttr(ym || '') + '"'
-                    + ' value="' + escAttr(show) + '"'
-                    + ' title="' + escAttr(title || '') + '"/>';
+                    + ' value="' + escAttr(show) + '"/>';
             }
             function scoreCellVal(row, textKey, valueKey) {
                 var text = row[textKey];
@@ -59,6 +58,77 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                 }
                 return String(v);
             }
+            function parseScoreInput(raw) {
+                var s = String(raw == null ? '' : raw).trim();
+                if (s === '') {
+                    return null;
+                }
+                var n = parseFloat(s);
+                if (isNaN(n)) {
+                    return null;
+                }
+                return Math.round(n * 100) / 100;
+            }
+            function formatScoreNum(n) {
+                if (n == null || isNaN(n)) {
+                    return '';
+                }
+                var t = Math.round(n * 100) / 100;
+                if (Math.abs(t - Math.round(t)) < 1e-9) {
+                    return String(Math.round(t));
+                }
+                return String(t);
+            }
+            /** A≥90  B70-89  C60-69  D&lt;60 */
+            function gradeFromFinal(score) {
+                if (score == null || isNaN(score)) {
+                    return '';
+                }
+                if (score >= 90) {
+                    return 'A';
+                }
+                if (score >= 70) {
+                    return 'B';
+                }
+                if (score >= 60) {
+                    return 'C';
+                }
+                return 'D';
+            }
+            function sumFourScores(q, p, d, v) {
+                var any = false;
+                var sum = 0;
+                [q, p, d, v].forEach(function (n) {
+                    if (n == null || isNaN(n)) {
+                        return;
+                    }
+                    any = true;
+                    sum += n;
+                });
+                return any ? Math.round(sum * 100) / 100 : null;
+            }
+            function findScoreRow($el) {
+                return $el.closest('tr');
+            }
+            function recalcRowFromInputs($tr, opts) {
+                opts = opts || {};
+                if (!$tr || !$tr.length) {
+                    return;
+                }
+                var q = parseScoreInput($tr.find('input.ssc-quality-score-input').val());
+                var p = parseScoreInput($tr.find('input.ssc-price-score-input').val());
+                var d = parseScoreInput($tr.find('input.ssc-delivery-score-input').val());
+                var v = parseScoreInput($tr.find('input.ssc-value-added-score-input').val());
+                var final = sumFourScores(q, p, d, v);
+                var $final = $tr.find('input.ssc-final-score-input');
+                var $grade = $tr.find('select.ssc-score-grade-select');
+                if ($final.length && opts.updateFinal !== false) {
+                    $final.val(final == null ? '' : formatScoreNum(final));
+                }
+                if ($grade.length && opts.updateGrade !== false) {
+                    $grade.val(gradeFromFinal(final));
+                }
+            }
             function rowYm(row) {
                 var v = String(row && row.ym ? row.ym : '').trim();
                 if (/^\d{4}-\d{2}$/.test(v)) {
@@ -78,6 +148,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                             quality_score: '',
                             price_score: '',
                             delivery_score: '',
+                            value_added_score: '',
                             final_score: '',
                             score_grade: ''
                         };
@@ -115,6 +186,14 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                     }
                     ensure(company, pickYm($el)).delivery_score = String($el.val() || '').trim();
                 });
+                $('#table').find('input.ssc-value-added-score-input').each(function () {
+                    var $el = $(this);
+                    var company = String($el.data('company') || '').trim();
+                    if (!company) {
+                        return;
+                    }
+                    ensure(company, pickYm($el)).value_added_score = String($el.val() || '').trim();
+                });
                 $('#table').find('input.ssc-final-score-input').each(function () {
                     var $el = $(this);
                     var company = String($el.data('company') || '').trim();
@@ -199,11 +278,11 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                         align: 'center',
                         halign: 'center',
                         class: 'text-center',
-                        width: 320
+                        width: 300
                     },
                     {
                         field: 'quality_score',
-                        title: '商务/技术得分',
+                        title: '质量得分',
                         operate: false,
                         sortable: false,
                         align: 'center',
@@ -212,7 +291,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                         width: 120,
                         formatter: function (value, row) {
                             var company = row.company_name || '';
-                            return scoreInputHtml(company, rowYm(row), 'ssc-quality-score-input', scoreCellVal(row, 'quality_score_text', 'quality_score'), '商务/技术得分(可改)');
+                            return scoreInputHtml(company, rowYm(row), 'ssc-quality-score-input', scoreCellVal(row, 'quality_score_text', 'quality_score'));
                         }
                     },
                     {
@@ -226,7 +305,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                         width: 120,
                         formatter: function (value, row) {
                             var company = row.company_name || '';
-                            return scoreInputHtml(company, rowYm(row), 'ssc-price-score-input', scoreCellVal(row, 'price_score_text', 'price_score'), '价格得分(可改)');
+                            return scoreInputHtml(company, rowYm(row), 'ssc-price-score-input', scoreCellVal(row, 'price_score_text', 'price_score'));
                         }
                     },
                     {
@@ -240,7 +319,21 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                         width: 120,
                         formatter: function (value, row) {
                             var company = row.company_name || '';
-                            return scoreInputHtml(company, rowYm(row), 'ssc-delivery-score-input', scoreCellVal(row, 'delivery_score_text', 'delivery_score'), '交货得分(可改)');
+                            return scoreInputHtml(company, rowYm(row), 'ssc-delivery-score-input', scoreCellVal(row, 'delivery_score_text', 'delivery_score'));
+                        }
+                    },
+                    {
+                        field: 'value_added_score',
+                        title: '增值服务',
+                        operate: false,
+                        sortable: false,
+                        align: 'center',
+                        halign: 'center',
+                        class: 'text-center',
+                        width: 120,
+                        formatter: function (value, row) {
+                            var company = row.company_name || '';
+                            return scoreInputHtml(company, rowYm(row), 'ssc-value-added-score-input', scoreCellVal(row, 'value_added_score_text', 'value_added_score'));
                         }
                     },
                     {
@@ -254,11 +347,17 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                         width: 130,
                         formatter: function (value, row) {
                             var company = row.company_name || '';
-                            var val = '';
-                            if (parseInt(row.final_score_saved, 10) === 1) {
-                                val = scoreCellVal(row, 'final_score_text', 'final_score');
+                            var val = scoreCellVal(row, 'final_score_text', 'final_score');
+                            if (val === '') {
+                                var sum = sumFourScores(
+                                    parseScoreInput(scoreCellVal(row, 'quality_score_text', 'quality_score')),
+                                    parseScoreInput(scoreCellVal(row, 'price_score_text', 'price_score')),
+                                    parseScoreInput(scoreCellVal(row, 'delivery_score_text', 'delivery_score')),
+                                    parseScoreInput(scoreCellVal(row, 'value_added_score_text', 'value_added_score'))
+                                );
+                                val = sum == null ? '' : formatScoreNum(sum);
                             }
-                            return scoreInputHtml(company, rowYm(row), 'ssc-final-score-input', val, '最终得分(可改)');
+                            return scoreInputHtml(company, rowYm(row), 'ssc-final-score-input', val);
                         }
                     },
                     {
@@ -275,7 +374,16 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                             var ym = rowYm(row);
                             var cur = String(row.score_grade || '').trim().toUpperCase();
                             if (['A', 'B', 'C', 'D'].indexOf(cur) < 0) {
-                                cur = '';
+                                var finalVal = parseScoreInput(scoreCellVal(row, 'final_score_text', 'final_score'));
+                                if (finalVal == null) {
+                                    finalVal = sumFourScores(
+                                        parseScoreInput(scoreCellVal(row, 'quality_score_text', 'quality_score')),
+                                        parseScoreInput(scoreCellVal(row, 'price_score_text', 'price_score')),
+                                        parseScoreInput(scoreCellVal(row, 'delivery_score_text', 'delivery_score')),
+                                        parseScoreInput(scoreCellVal(row, 'value_added_score_text', 'value_added_score'))
+                                    );
+                                }
+                                cur = gradeFromFinal(finalVal);
                             }
                             var opts = [
                                 {v: '', t: '请选择'},
@@ -286,8 +394,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                             ];
                             var html = '<select class="form-control input-sm ssc-score-grade-select"'
                                 + ' data-company="' + escAttr(company) + '"'
-                                + ' data-ym="' + escAttr(ym) + '"'
-                                + ' title="评分等级">';
+                                + ' data-ym="' + escAttr(ym) + '">';
                             opts.forEach(function (o) {
                                 html += '<option value="' + o.v + '"' + (cur === o.v ? ' selected' : '') + '>'
                                     + o.t + '</option>';
@@ -318,6 +425,28 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                             }
                             return v || '—';
                         }
+                    },
+                    {
+                        field: 'operate',
+                        title: '操作',
+                        operate: false,
+                        sortable: false,
+                        align: 'center',
+                        halign: 'center',
+                        valign: 'middle',
+                        class: 'text-center',
+                        width: 130,
+                        formatter: function (value, row) {
+                            var company = String(row && row.company_name ? row.company_name : '').trim();
+                            if (!company) {
+                                return '';
+                            }
+                            var ym = rowYm(row);
+                            return '<a href="javascript:;" class="btn btn-xs btn-info btn-ssc-month-orders"'
+                                + ' data-company="' + escAttr(company) + '"'
+                                + ' data-ym="' + escAttr(ym) + '"'
+                                + ' title="查看该供应商本月订单">本月订单明细</a>';
+                        }
                     }
                 ]],
                 queryParams: function (params) {
@@ -337,6 +466,40 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
             });
             Table.api.bindevent(table);
 
+            // 四项分数变动 → 自动汇总最终得分并定级
+            $(document).off('input.sscAutoScore change.sscAutoScore', '#table')
+                .on('input.sscAutoScore change.sscAutoScore', '#table input.ssc-quality-score-input, #table input.ssc-price-score-input, #table input.ssc-delivery-score-input, #table input.ssc-value-added-score-input', function () {
+                    recalcRowFromInputs(findScoreRow($(this)));
+                });
+            // 手工改最终得分 → 同步定级(仍可再手动改等级)
+            $(document).off('input.sscFinalGrade change.sscFinalGrade', '#table')
+                .on('input.sscFinalGrade change.sscFinalGrade', '#table input.ssc-final-score-input', function () {
+                    var $tr = findScoreRow($(this));
+                    var final = parseScoreInput($(this).val());
+                    $tr.find('select.ssc-score-grade-select').val(gradeFromFinal(final));
+                });
+
+            $(document).off('click.sscMonthOrders', '.btn-ssc-month-orders').on('click.sscMonthOrders', '.btn-ssc-month-orders', function (e) {
+                e.preventDefault();
+                e.stopPropagation();
+                var company = String($(this).data('company') || '').trim();
+                var ym = normalizeYm($(this).data('ym')) || resolveYm();
+                if (!company) {
+                    Toastr.warning('供应商名称无效');
+                    return false;
+                }
+                if (!/^\d{4}-\d{2}$/.test(ym)) {
+                    Toastr.warning('月份无效');
+                    return false;
+                }
+                var url = Fast.api.fixurl('supplierservicescore/monthorders')
+                    + '?ym=' + encodeURIComponent(ym)
+                    + '&company_name=' + encodeURIComponent(company);
+                var title = '本月订单明细';
+                Backend.api.open(url, title, {area: ['92%', '82%'], maxmin: true});
+                return false;
+            });
+
             $(document).off('change.supplierScoreYm', '#export-review-ym').on('change.supplierScoreYm', '#export-review-ym', function () {
                 var ym = normalizeYm($(this).val());
                 if (ym && $(this).val() !== ym) {
@@ -394,6 +557,33 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                 });
                 return false;
             });
+        },
+
+        /**
+         * 本月订单明细弹层:按开标/指定时间列出各订单,点详情打开订单详情
+         */
+        monthorders: function () {
+            $(document).off('click.sscOrderDetails', '.btn-ssc-order-details').on('click.sscOrderDetails', '.btn-ssc-order-details', function (e) {
+                e.preventDefault();
+                e.stopPropagation();
+                var sid = $(this).attr('data-scydgy-id') || $(this).data('scydgy-id');
+                if (sid == null || String(sid).trim() === '' || String(sid) === '0') {
+                    Toastr.warning('无法打开详情:缺少工序信息');
+                    return false;
+                }
+                var win = window.top || window.parent || window;
+                var backendApi = (win.Backend && win.Backend.api) ? win.Backend.api
+                    : ((typeof Backend !== 'undefined' && Backend.api) ? Backend.api : null);
+                var fastApi = (win.Fast && win.Fast.api) ? win.Fast.api
+                    : ((typeof Fast !== 'undefined' && Fast.api) ? Fast.api : null);
+                if (!backendApi || !fastApi || typeof backendApi.open !== 'function' || typeof fastApi.fixurl !== 'function') {
+                    Toastr.error('详情组件未就绪,请刷新后重试');
+                    return false;
+                }
+                var url = fastApi.fixurl('procuremen/details?ids=' + encodeURIComponent(String(sid)));
+                backendApi.open(url, '详情', {area: ['90%', '85%'], maxmin: true});
+                return false;
+            });
         }
     };
     return Controller;

Некоторые файлы не были показаны из-за большого количества измененных файлов