m0_70156489 2 dni temu
rodzic
commit
89f4c3b340

+ 15 - 1
application/admin/controller/Customer.php

@@ -99,7 +99,7 @@ class Customer extends Backend
 
             foreach ($list as $row) {
                 $row->visible([
-                    'id', 'company_name', 'username', 'account', 'email', 'phone', 'company_type',
+                    'id', 'company_name', 'username', 'account', 'email', 'phone', 'score', 'company_type',
                     'createtime', 'updatetime', 'status',
                 ]);
             }
@@ -163,6 +163,12 @@ class Customer extends Backend
             $status = '1';
         }
 
+        $scoreRaw = trim((string)($params['score'] ?? ''));
+        $score = $scoreRaw === '' ? null : $scoreRaw;
+        if ($score !== null && !preg_match('/^-?\d+(\.\d+)?$/', (string)$score)) {
+            $this->error('考核评分值须为数字');
+        }
+
         $now = date('Y-m-d H:i:s');
         $data = [
             'company_name' => trim((string)($params['company_name'] ?? '')),
@@ -171,6 +177,7 @@ class Customer extends Backend
             'account'      => $account,
             'email'        => $email,
             'password'     => md5(md5($pwd)),
+            'score'        => $score,
             'company_type' => trim((string)($params['company_type'] ?? '')),
             'status'       => $status,
             'createtime'   => $now,
@@ -253,6 +260,12 @@ class Customer extends Backend
             $status = '1';
         }
 
+        $scoreRaw = trim((string)($params['score'] ?? ''));
+        $score = $scoreRaw === '' ? null : $scoreRaw;
+        if ($score !== null && !preg_match('/^-?\d+(\.\d+)?$/', (string)$score)) {
+            $this->error('考核评分值须为数字');
+        }
+
         $data = [
             'id'           => $rowId,
             'company_name' => trim((string)($params['company_name'] ?? '')),
@@ -260,6 +273,7 @@ class Customer extends Backend
             'phone'        => $phone,
             'account'      => $account,
             'email'        => $email,
+            'score'        => $score,
             'company_type' => trim((string)($params['company_type'] ?? '')),
             'status'       => $status,
             'updatetime'   => date('Y-m-d H:i:s'),

+ 489 - 19
application/admin/controller/Procuremen.php

@@ -1896,6 +1896,26 @@ class Procuremen extends Backend
         return implode("\n", $lines);
     }
 
+    /**
+     * 选商类弹窗:轻量壳(无后台菜单外壳),初选/补加样式与高度一致
+     */
+    protected function fetchProcuremenDialogLite(string $view, string $title = ''): string
+    {
+        $restoreLayout = !empty($this->layout) ? ('layout/' . $this->layout) : false;
+        $this->view->engine->layout(false);
+        try {
+            $bodyHtml = $this->view->fetch($view);
+            $this->view->assign('dialogBody', $bodyHtml);
+            $this->view->assign('dialogTitle', $title !== '' ? $title : '选商');
+
+            return $this->view->fetch('procuremen/dialog_lite_shell');
+        } finally {
+            if ($restoreLayout) {
+                $this->view->engine->layout($restoreLayout);
+            }
+        }
+    }
+
     /**
      * 从订单 bundle 取报价截止时间(多工序取首个有效 sys_rq)
      */
@@ -1945,7 +1965,48 @@ class Procuremen extends Backend
     }
 
     /**
-     * 截止时间前:隐藏单价/交期真实值,统一展示文案
+     * 解析预估工期(天):优先明细 lead_days,否则按交货日期相对今天推算
+     *
+     * @param array<string, mixed> $d
+     * @param string               $deliveryYmd
+     */
+    protected function resolveProcuremenLeadDays(array $d, string $deliveryYmd = ''): ?int
+    {
+        if (array_key_exists('lead_days', $d) && $d['lead_days'] !== null && $d['lead_days'] !== '' && is_numeric($d['lead_days'])) {
+            return max(0, (int)$d['lead_days']);
+        }
+        if ($deliveryYmd === '') {
+            $deliveryYmd = $this->formatDeliveryYmd($d['delivery'] ?? null);
+        }
+        if ($deliveryYmd === '' || !preg_match('/^(\d{4}-\d{2}-\d{2})/', $deliveryYmd, $m)) {
+            return null;
+        }
+        $t1 = strtotime(date('Y-m-d') . ' 00:00:00');
+        $t2 = strtotime($m[1] . ' 00:00:00');
+        if ($t1 === false || $t2 === false) {
+            return null;
+        }
+
+        return max(0, (int)round(($t2 - $t1) / 86400));
+    }
+
+    /**
+     * @param int|null $days
+     */
+    protected function formatProcuremenLeadDaysText($days): string
+    {
+        if ($days === null || $days === '') {
+            return '未填写';
+        }
+        if (!is_numeric($days)) {
+            return '未填写';
+        }
+
+        return ((int)$days) . '天';
+    }
+
+    /**
+     * 未开标验证前:隐藏单价/交期/工期/小计/操作时间真实值,统一展示文案
      *
      * @param array<string, mixed> $line
      */
@@ -1953,6 +2014,8 @@ class Procuremen extends Backend
     {
         $amountFilled = !empty($line['amount_filled']);
         $deliveryFilled = !empty($line['delivery_filled']);
+        $leadFilled = !empty($line['lead_days_filled']);
+        $operFilled = trim((string)($line['oper_time_text'] ?? '')) !== '';
 
         $line['unit_price_text'] = $amountFilled ? '开标验证后查看' : '未填写';
         $line['amount_show'] = '';
@@ -1967,17 +2030,27 @@ class Procuremen extends Backend
         }
         $line['delivery_ymd'] = '';
 
-        $line['subtotal_text'] = $amountFilled ? '' : '';
+        $leadText = $leadFilled ? '开标验证后查看' : '未填写';
+        $line['lead_days_show'] = $leadText;
+        $line['lead_days_text'] = $leadText;
+        $line['lead_days'] = '';
+
+        // 小计、操作时间与单价等同属报价信息,开标前不可见
+        $line['subtotal_text'] = $amountFilled ? '开标验证后查看' : '';
         $line['subtotal_num'] = null;
+        $line['oper_time_text'] = $operFilled ? '开标验证后查看' : '';
 
         $line['amount_quote_pending'] = $amountFilled ? 1 : 0;
         $line['delivery_quote_pending'] = $deliveryFilled ? 1 : 0;
-        $line['quote_before_deadline'] = ($amountFilled || $deliveryFilled) ? 1 : 0;
+        $line['lead_days_quote_pending'] = $leadFilled ? 1 : 0;
+        $line['oper_time_quote_pending'] = $operFilled ? 1 : 0;
+        $line['quote_before_deadline'] = ($amountFilled || $deliveryFilled || $leadFilled || $operFilled) ? 1 : 0;
         $line['amount_filled'] = false;
         $line['delivery_filled'] = false;
+        $line['lead_days_filled'] = false;
         $line['is_quoted'] = false;
         if (array_key_exists('quote_label', $line)) {
-            $line['quote_label'] = ($amountFilled || $deliveryFilled) ? '未开标验证' : '未报价';
+            $line['quote_label'] = ($amountFilled || $deliveryFilled || $leadFilled || $operFilled) ? '未开标验证' : '未报价';
         }
     }
 
@@ -2434,6 +2507,18 @@ class Procuremen extends Backend
         if (!array_key_exists('delivery_filled', $line)) {
             $line['delivery_filled'] = 0;
         }
+        if (!array_key_exists('lead_days_filled', $line)) {
+            $line['lead_days_filled'] = 0;
+        }
+        if (!array_key_exists('lead_days_quote_pending', $line)) {
+            $line['lead_days_quote_pending'] = 0;
+        }
+        if (!array_key_exists('lead_days_show', $line)) {
+            $line['lead_days_show'] = '未填写';
+        }
+        if (!array_key_exists('lead_days_text', $line)) {
+            $line['lead_days_text'] = $line['lead_days_show'];
+        }
 
         return $line;
     }
@@ -2569,6 +2654,9 @@ class Procuremen extends Backend
             $unitPriceText = $amountFilled
                 ? ($amountNum !== null ? $this->formatProcuremenMoneyDisplay($amountNum) : $am)
                 : '未填写';
+            $leadDays = $this->resolveProcuremenLeadDays($d, $deliveryFilled ? $deliveryShow : '');
+            $leadFilled = ($leadDays !== null);
+            $leadText = $this->formatProcuremenLeadDaysText($leadDays);
             $lineRow = [
                 'cgymc'           => $gymc,
                 'amount'          => $am,
@@ -2579,11 +2667,16 @@ class Procuremen extends Backend
                 'delivery_show'   => $deliveryFilled ? $deliveryShow : '未填写',
                 'delivery_filled' => $deliveryFilled,
                 'delivery_ymd'    => $deliveryFilled ? $deliveryShow : '',
+                'lead_days'       => $leadFilled ? $leadDays : '',
+                'lead_days_filled'=> $leadFilled,
+                'lead_days_show'  => $leadText,
+                'lead_days_text'  => $leadText,
                 'subtotal_text'   => $subtotal !== null ? $this->formatProcuremenMoneyDisplay($subtotal) : '',
                 'subtotal_num'    => $subtotal,
                 'status_name'     => trim((string)($d['status_name'] ?? '')),
                 'is_quoted'       => $amountFilled,
                 'quote_label'     => $amountFilled ? '已报价' : '未报价',
+                'oper_time_text'  => $this->resolveDetailSupplierOperTime($d),
             ];
             if (!$quoteVisible) {
                 $this->maskSupplierQuoteLineBeforeDeadline($lineRow);
@@ -2775,6 +2868,9 @@ class Procuremen extends Backend
             $unitPriceText = $amountFilled
                 ? ($amountNum !== null ? $this->formatProcuremenMoneyDisplay($amountNum) : $am)
                 : '未填写';
+            $leadDays = $this->resolveProcuremenLeadDays($d, $deliveryFilled ? $deliveryShow : '');
+            $leadFilled = ($leadDays !== null);
+            $leadText = $this->formatProcuremenLeadDaysText($leadDays);
             $pickLine = [
                 'cgymc'           => $gymc,
                 'amount_text'     => $amountFilled ? ($amountNum !== null ? $this->formatProcuremenMoneyDisplay($amountNum) : $am) : '未填写',
@@ -2782,8 +2878,13 @@ class Procuremen extends Backend
                 'amount_filled'   => $amountFilled,
                 'delivery_filled' => $deliveryFilled,
                 'unit_price_text' => $unitPriceText,
+                'lead_days'       => $leadFilled ? $leadDays : '',
+                'lead_days_filled'=> $leadFilled,
+                'lead_days_show'  => $leadText,
+                'lead_days_text'  => $leadText,
                 'subtotal_text'   => $subtotal !== null ? $this->formatProcuremenMoneyDisplay($subtotal) : '',
                 'subtotal_num'    => $subtotal,
+                'oper_time_text'  => $this->resolveDetailSupplierOperTime($d),
                 'is_quoted'       => $amountFilled,
             ];
             if (!$quoteVisible) {
@@ -4467,7 +4568,7 @@ class Procuremen extends Backend
         if ($sysRqRaw !== '' && stripos($sysRqRaw, '0000-00-00') !== 0) {
             $deadlineDisp = $this->formatProcuremenSysRqForDisplay($sysRqRaw);
             if ($deadlineDisp !== '' && $deadlineDisp !== '—') {
-                $deadlineText = '截止 ' . $deadlineDisp;
+                $deadlineText = '招标截止 ' . $deadlineDisp;
             }
         }
 
@@ -5548,6 +5649,9 @@ class Procuremen extends Backend
                     $sub = $this->calcProcuremenDetailSubtotal($d['amount'] ?? null, $qtyStr);
                 }
                 $ct = $this->resolveDetailSupplierOperTime($d);
+                $leadDays = $this->resolveProcuremenLeadDays($d, $deliveryFilled ? (string)$deliveryYmd : '');
+                $leadFilled = ($leadDays !== null);
+                $leadText = $this->formatProcuremenLeadDaysText($leadDays);
                 $line = [
                     'cgymc'           => $gymcBySid[$sid] ?? trim((string)($d['CGYMC'] ?? '')),
                     'unit_price_text' => $amountFilled
@@ -5558,6 +5662,10 @@ class Procuremen extends Backend
                     'amount_filled'   => $amountFilled,
                     'delivery_filled' => $deliveryFilled,
                     'delivery_show'   => $deliveryFilled ? $deliveryYmd : '未填写',
+                    'lead_days'       => $leadFilled ? $leadDays : '',
+                    'lead_days_filled'=> $leadFilled,
+                    'lead_days_show'  => $leadText,
+                    'lead_days_text'  => $leadText,
                     'status'          => ProcuremenStatus::normalizePodPickStatus($d['status'] ?? ''),
                     'is_picked'       => ProcuremenStatus::isPodPicked($d['status'] ?? ''),
                     'is_void'         => $isVoid,
@@ -5927,18 +6035,108 @@ class Procuremen extends Backend
         $sysRq = trim($sysRq);
         if ($sysRq === '') {
             if ($required) {
-                $this->error('请选择截止时间');
+                $this->error('请选择招标截止日期');
             }
             return '';
         }
         $ts = strtotime($sysRq);
         if ($ts === false || $ts <= 0) {
-            $this->error('截止时间格式无效');
+            $this->error('招标截止日期格式无效');
+        }
+        $ymd = date('Y-m-d', $ts);
+        if ($ymd < date('Y-m-d')) {
+            $this->error('招标截止日期不能早于今天');
+        }
+        if ($ts <= time()) {
+            $this->error('招标截止时间不能早于当前时间');
         }
 
         return date('Y-m-d H:i:s', $ts);
     }
 
+    /**
+     * 确保 purchase_order.delivery_deadline 存在
+     */
+    protected function ensurePurchaseOrderDeliveryDeadlineColumn(): void
+    {
+        static $done = false;
+        if ($done) {
+            return;
+        }
+        $done = true;
+        try {
+            $cols = Db::query("SHOW COLUMNS FROM `purchase_order` LIKE 'delivery_deadline'");
+            if (empty($cols)) {
+                Db::execute(
+                    "ALTER TABLE `purchase_order` ADD COLUMN `delivery_deadline` date DEFAULT NULL COMMENT '交货截止日期' AFTER `sys_rq`"
+                );
+            }
+        } catch (\Throwable $e) {
+            // ignore
+        }
+    }
+
+    /**
+     * 解析交货截止日期(仅年月日)
+     */
+    protected function parseProcuremenDeliveryDeadlineInput(string $raw, bool $required = true): string
+    {
+        $raw = trim($raw);
+        if ($raw === '') {
+            if ($required) {
+                $this->error('请选择交货截止日期');
+            }
+
+            return '';
+        }
+        if (preg_match('/^(\d{4})[\/.\-](\d{1,2})[\/.\-](\d{1,2})/', $raw, $m)) {
+            $ymd = sprintf('%04d-%02d-%02d', (int)$m[1], (int)$m[2], (int)$m[3]);
+        } else {
+            $ts = strtotime(str_replace('/', '-', $raw));
+            if ($ts === false || $ts <= 0) {
+                $this->error('交货截止日期格式无效');
+            }
+            $ymd = date('Y-m-d', $ts);
+        }
+        if ($ymd < date('Y-m-d')) {
+            $this->error('交货截止日期不能早于今天');
+        }
+
+        return $ymd;
+    }
+
+    protected function formatProcuremenDeliveryDeadlineForInput($value): string
+    {
+        $s = trim((string)$value);
+        if ($s === '' || stripos($s, '0000-00-00') === 0) {
+            return '';
+        }
+        if (preg_match('/^(\d{4}-\d{2}-\d{2})/', $s, $m)) {
+            return $m[1];
+        }
+        $ts = strtotime(str_replace('/', '-', $s));
+
+        return ($ts !== false && $ts > 0) ? date('Y-m-d', $ts) : '';
+    }
+
+    /**
+     * @param array{pos?:array} $bundle
+     */
+    protected function resolveBundleDeliveryDeadline(array $bundle): string
+    {
+        foreach ($bundle['pos'] ?? [] as $po) {
+            if (!is_array($po)) {
+                continue;
+            }
+            $d = $this->formatProcuremenDeliveryDeadlineForInput($po['delivery_deadline'] ?? '');
+            if ($d !== '') {
+                return $d;
+            }
+        }
+
+        return '';
+    }
+
     protected function formatProcuremenSysRqForInput($sysRq): string
     {
         $sr = trim((string)$sysRq);
@@ -5980,7 +6178,8 @@ class Procuremen extends Backend
         string $sysRqDb,
         $wflowStatus = null,
         ?string $pickTime = null,
-        ?array $exists = null
+        ?array $exists = null,
+        ?string $deliveryDeadline = null
     ): void {
         $ids = $this->extractScydgyRowId($row);
         if (!$this->isValidScydgyRowId($ids)) {
@@ -5989,6 +6188,7 @@ class Procuremen extends Backend
         if ($exists === null) {
             $exists = Db::table('purchase_order')->where('scydgy_id', $ids)->find();
         }
+        $this->ensurePurchaseOrderDeliveryDeadlineColumn();
         $data = [
             'scydgy_id'     => $ids,
             'CCYDH'         => $row['CCYDH'] ?? null,
@@ -6012,6 +6212,9 @@ class Procuremen extends Backend
             'status'        => ProcuremenStatus::PO_IN_PROGRESS,
             'sys_rq'        => $sysRqDb,
         ];
+        if ($deliveryDeadline !== null) {
+            $data['delivery_deadline'] = $deliveryDeadline !== '' ? $deliveryDeadline : null;
+        }
         if ($wflowStatus !== null && $wflowStatus !== '') {
             $data['wflow_status'] = ProcuremenStatus::normalizeWflowStatus($wflowStatus);
         }
@@ -6407,6 +6610,46 @@ class Procuremen extends Backend
                 'notify_preview' => $this->notifyDryRunPreview,
             ]);
         }
+
+        $nameLabels = [];
+        foreach ($targets as $g) {
+            if (!is_array($g)) {
+                continue;
+            }
+            $n = trim((string)($g['name'] ?? ''));
+            if ($n !== '') {
+                $nameLabels[] = $n;
+            }
+        }
+        $logAction = $sendSms ? 'audit_resend_sms' : 'audit_resend_email';
+        $logMsg = '确认页重新发送' . $successVerb . '(' . count($nameLabels) . ' 家)'
+            . ($nameLabels !== [] ? (':' . implode('、', $nameLabels)) : '');
+        $seenLogSid = [];
+        foreach ($bundle['pos'] ?? [] as $poRow) {
+            if (!is_array($poRow)) {
+                continue;
+            }
+            $sid = (int)($poRow['scydgy_id'] ?? 0);
+            if (!$this->isValidScydgyRowId($sid) || isset($seenLogSid[$sid])) {
+                continue;
+            }
+            $seenLogSid[$sid] = true;
+            $poIdLog = (int)($poRow['id'] ?? $poRow['ID'] ?? 0);
+            $this->addOrderLog($sid, $logAction, $logMsg, $poIdLog > 0 ? $poIdLog : null);
+        }
+        if ($seenLogSid === []) {
+            foreach ($mergeRows as $mr) {
+                if (!is_array($mr)) {
+                    continue;
+                }
+                $sid = $this->extractScydgyRowId($mr);
+                if ($this->isValidScydgyRowId($sid)) {
+                    $this->addOrderLog($sid, $logAction, $logMsg, null);
+                    break;
+                }
+            }
+        }
+
         $this->success('已向 ' . count($targets) . ' 家供应商重新发送' . $successVerb);
     }
 
@@ -6698,7 +6941,7 @@ class Procuremen extends Backend
         }
         $this->view->assign('pickMode', 1);
 
-        return $this->view->fetch('procuremen/review');
+        return $this->fetchProcuremenDialogLite('procuremen/review', '初选供应商');
     }
 
     /**
@@ -6989,13 +7232,13 @@ class Procuremen extends Backend
 
         $sysRq = trim((string)$this->request->post('sys_rq', ''));
         if ($sysRq === '') {
-            $this->error('请选择截止时间');
-        }
-        $sysRqTs = strtotime($sysRq);
-        if ($sysRqTs === false || $sysRqTs <= 0) {
-            $this->error('截止时间格式无效');
+            $this->error('请选择招标截止日期');
         }
-        $sysRqDb = date('Y-m-d H:i:s', $sysRqTs);
+        $sysRqDb = $this->parseProcuremenSysRqInput($sysRq, true);
+        $deliveryDeadlineDb = $this->parseProcuremenDeliveryDeadlineInput(
+            (string)$this->request->post('delivery_deadline', ''),
+            true
+        );
 
         $isMerge = count($mergeRows) > 1;
         $ccydhLog = '';
@@ -7060,7 +7303,8 @@ class Procuremen extends Backend
                     $sysRqDb,
                     ProcuremenStatus::WFLOW_PENDING_CONFIRM,
                     $issueTime,
-                    $this->isValidScydgyRowId($sid) ? ($existingBySid[$sid] ?? null) : null
+                    $this->isValidScydgyRowId($sid) ? ($existingBySid[$sid] ?? null) : null,
+                    $deliveryDeadlineDb
                 );
             }
             foreach ($issueCompanies as $c) {
@@ -7245,12 +7489,235 @@ class Procuremen extends Backend
             $sysRqForView = $this->formatProcuremenSysRqForInput($this->resolveBundleSysRq($bundle));
         }
         $this->view->assign('sysRq', $sysRqForView);
+        $deliveryDeadlineForView = $this->formatProcuremenDeliveryDeadlineForInput($po['delivery_deadline'] ?? null);
+        if ($deliveryDeadlineForView === '') {
+            $deliveryDeadlineForView = $this->resolveBundleDeliveryDeadline($bundle);
+        }
+        $this->view->assign('deliveryDeadline', $deliveryDeadlineForView);
         $this->view->assign('bidOpenVerified', $this->isProcuremenBidOpenVerifiedForBundle($bundle) ? 1 : 0);
-        $this->view->assign('quoteDeadlineReached', $this->isProcuremenQuoteDeadlineReached($this->resolveBundleSysRq($bundle)) ? 1 : 0);
+        $quoteDeadlineReached = $this->isProcuremenQuoteDeadlineReached($this->resolveBundleSysRq($bundle)) ? 1 : 0;
+        $this->view->assign('quoteDeadlineReached', $quoteDeadlineReached);
+        $this->view->assign('canAppendSupplier', $this->canProcuremenAppendSupplierForBundle($bundle) ? 1 : 0);
+        $this->view->assign('appendSupplierDeadlinePassed', $this->canProcuremenAppendSupplierActionForBundle($bundle) ? 0 : 1);
 
         return $this->view->fetch('procuremen/audit_issue');
     }
 
+    /**
+     * 确认页是否显示补加供应商按钮:未开标即可显示(超截止点击时再提示)
+     *
+     * @param array{ccydh?:string,pos?:array,merge_rows?:array} $bundle
+     */
+    protected function canProcuremenAppendSupplierForBundle(array $bundle): bool
+    {
+        return !$this->isProcuremenBidOpenVerifiedForBundle($bundle);
+    }
+
+    /**
+     * 是否允许实际执行补加:未开标且未过招标截止(无截止时间时允许)
+     *
+     * @param array{ccydh?:string,pos?:array,merge_rows?:array} $bundle
+     */
+    protected function canProcuremenAppendSupplierActionForBundle(array $bundle): bool
+    {
+        if ($this->isProcuremenBidOpenVerifiedForBundle($bundle)) {
+            return false;
+        }
+        $sysRq = trim((string)$this->resolveBundleSysRq($bundle));
+        if ($sysRq === '' || preg_match('/^0000-00-00/i', $sysRq)) {
+            return true;
+        }
+
+        return !$this->isProcuremenQuoteDeadlineReached($sysRq);
+    }
+
+    /**
+     * 确认页 — 补加供应商弹窗
+     */
+    public function auditAppend()
+    {
+        if (!$this->hasProcuremenPermStrict(['auditappend'])) {
+            $this->error(__('You have no permission'));
+        }
+        $ids = $this->request->param('scydgy_id', $this->request->param('ids', $this->request->param('id', '')));
+        if (is_array($ids)) {
+            $ids = isset($ids[0]) ? $ids[0] : '';
+        }
+        $ids = trim((string)$ids);
+        if ($ids === '') {
+            $this->error(__('Invalid parameters'));
+        }
+        if ($this->request->isPost()) {
+            $this->error('请使用弹窗内「确认」提交');
+        }
+        $bundle = $this->loadAuditOrderBundleByScydgyId($ids);
+        if (($bundle['pos'] ?? []) === []) {
+            $this->error('该订单不在待确认供应商状态');
+        }
+        if (!$this->canProcuremenAppendSupplierActionForBundle($bundle)) {
+            if ($this->isProcuremenBidOpenVerifiedForBundle($bundle)) {
+                $this->error('已开标验证,不能再补加供应商');
+            }
+            $this->error('已经超过了招标截止日期,不可操作');
+        }
+        $quoteGroups = $this->loadAuditSupplierQuoteGroups($bundle);
+        $existingNames = [];
+        foreach ($quoteGroups as $g) {
+            if (!is_array($g)) {
+                continue;
+            }
+            $n = trim((string)($g['company_name'] ?? $g['name'] ?? ''));
+            if ($n !== '') {
+                $existingNames[$n] = true;
+            }
+        }
+        $po = is_array($bundle['pos'][0] ?? null) ? $bundle['pos'][0] : [];
+        $mergeRows = $bundle['merge_rows'] ?? [];
+        $this->view->assign('scydgyId', $ids);
+        $this->view->assign('orderCcydh', $bundle['ccydh'] ?? '');
+        $this->view->assign('po', $po);
+        $this->view->assign('processDisplayRows', $this->buildAuditProcessDisplayRows(is_array($mergeRows) ? $mergeRows : []));
+        $this->view->assign('existingCompanyNames', array_keys($existingNames));
+        $this->view->assign('existingCompanyNamesJson', json_encode(array_keys($existingNames), JSON_UNESCAPED_UNICODE));
+        $sysRqForView = $this->formatProcuremenSysRqForInput($po['sys_rq'] ?? null);
+        if ($sysRqForView === '') {
+            $sysRqForView = $this->formatProcuremenSysRqForInput($this->resolveBundleSysRq($bundle));
+        }
+        $this->view->assign('sysRq', $sysRqForView);
+        $deliveryDeadlineForView = $this->formatProcuremenDeliveryDeadlineForInput($po['delivery_deadline'] ?? '');
+        if ($deliveryDeadlineForView === '') {
+            $deliveryDeadlineForView = $this->resolveBundleDeliveryDeadline($bundle);
+        }
+        $this->view->assign('deliveryDeadline', $deliveryDeadlineForView);
+
+        return $this->fetchProcuremenDialogLite('procuremen/audit_append', '补加供应商');
+    }
+
+    /**
+     * 确认页 — 补加供应商提交
+     */
+    public function auditAppendSubmit()
+    {
+        if (!$this->request->isPost()) {
+            $this->error(__('Invalid parameters'));
+        }
+        if (!$this->hasProcuremenPermStrict(['auditappendsubmit', 'auditappend'])) {
+            $this->error(__('You have no permission'));
+        }
+        $ids = trim((string)$this->request->post('scydgy_id', $this->request->post('ids', '')));
+        if ($ids === '') {
+            $this->error(__('Invalid parameters'));
+        }
+        $bundle = $this->loadAuditOrderBundleByScydgyId($ids);
+        if (($bundle['pos'] ?? []) === []) {
+            $this->error('该订单不在待确认供应商状态');
+        }
+        if (!$this->canProcuremenAppendSupplierActionForBundle($bundle)) {
+            if ($this->isProcuremenBidOpenVerifiedForBundle($bundle)) {
+                $this->error('已开标验证,不能再补加供应商');
+            }
+            $this->error('已经超过了招标截止日期,不可操作');
+        }
+        $companiesJson = $this->request->post('companies_json', '[]');
+        $companies = json_decode((string)$companiesJson, true);
+        if (!is_array($companies)) {
+            $this->error('供应商数据无效');
+        }
+        $issueCompanies = $this->normalizePickCompanies($companies);
+        if ($issueCompanies === []) {
+            $this->error('请至少选择一家合作供应商');
+        }
+        $mergeRows = $bundle['merge_rows'] ?? [];
+        if (!is_array($mergeRows) || $mergeRows === []) {
+            $this->error('未找到工序行,无法补加');
+        }
+
+        $existingNames = [];
+        foreach ($this->loadAuditSupplierQuoteGroups($bundle) as $g) {
+            if (!is_array($g)) {
+                continue;
+            }
+            $n = trim((string)($g['company_name'] ?? ''));
+            if ($n !== '') {
+                $existingNames[$n] = true;
+            }
+        }
+        $toAdd = [];
+        foreach ($issueCompanies as $c) {
+            if (!is_array($c)) {
+                continue;
+            }
+            $n = trim((string)($c['name'] ?? ''));
+            if ($n === '' || isset($existingNames[$n])) {
+                continue;
+            }
+            $toAdd[] = $c;
+        }
+        if ($toAdd === []) {
+            $this->error('所选供应商均已在本单中,请选择新的供应商');
+        }
+
+        $addedNames = [];
+        Db::startTrans();
+        try {
+            foreach ($toAdd as $c) {
+                $this->issuePersistSupplierDetails($c, $mergeRows, false, true);
+                $addedNames[] = trim((string)($c['name'] ?? ''));
+            }
+            Db::commit();
+        } catch (\Throwable $e) {
+            Db::rollback();
+            $this->error($e->getMessage());
+        }
+
+        $notifyErrors = [];
+        $notifyBundleBase = [
+            'ccydh'      => $bundle['ccydh'] ?? '',
+            'pos'        => $bundle['pos'] ?? [],
+            'merge_rows' => $mergeRows,
+        ];
+        foreach ($toAdd as $c) {
+            if (!is_array($c)) {
+                continue;
+            }
+            $cname = trim((string)($c['name'] ?? ''));
+            if ($cname === '') {
+                continue;
+            }
+            try {
+                $this->sendAuditSupplierNotifyChannels($c, $mergeRows, $notifyBundleBase, true, true, 'audit_append');
+            } catch (\Throwable $e) {
+                $notifyErrors[] = $cname . ':' . $e->getMessage();
+            }
+        }
+
+        $logMsg = '补加供应商(' . count($toAdd) . ' 家):' . implode('、', array_filter($addedNames));
+        $seenLogSid = [];
+        foreach ($bundle['pos'] ?? [] as $poRow) {
+            if (!is_array($poRow)) {
+                continue;
+            }
+            $sid = (int)($poRow['scydgy_id'] ?? 0);
+            if (!$this->isValidScydgyRowId($sid) || isset($seenLogSid[$sid])) {
+                continue;
+            }
+            $seenLogSid[$sid] = true;
+            $poIdLog = (int)($poRow['id'] ?? $poRow['ID'] ?? 0);
+            $this->addOrderLog($sid, 'audit_append_supplier', $logMsg, $poIdLog > 0 ? $poIdLog : null);
+        }
+        if ($seenLogSid === []) {
+            $logSid = $this->extractScydgyRowId(is_array($mergeRows[0] ?? null) ? $mergeRows[0] : []);
+            if ($this->isValidScydgyRowId($logSid)) {
+                $this->addOrderLog($logSid, 'audit_append_supplier', $logMsg, null);
+            }
+        }
+
+        if ($notifyErrors !== [] && !$this->isProcuremenNotifyDryRun()) {
+            $this->error('已写入明细,但部分通知失败:' . implode(';', $notifyErrors));
+        }
+        $this->success('已补加 ' . count($toAdd) . ' 家供应商并发送通知');
+    }
+
     /**
      * 开标双重验证 — 可选验证人列表
      */
@@ -7892,7 +8359,7 @@ class Procuremen extends Backend
         }
         $this->view->assign('pickMode', 0);
 
-        return $this->view->fetch('procuremen/review');
+        return $this->fetchProcuremenDialogLite('procuremen/review', '选商');
     }
 
     /**
@@ -7900,7 +8367,7 @@ class Procuremen extends Backend
      */
     public function reviewCompanies()
     {
-        if (!$this->hasProcuremenPerm(['reviewcompanies', 'pickreview', 'picksubmit', 'review'])) {
+        if (!$this->hasProcuremenPerm(['reviewcompanies', 'pickreview', 'picksubmit', 'review', 'auditissue', 'auditappend', 'auditappendsubmit'])) {
             $this->error(__('You have no permission'));
         }
         if (!$this->request->isAjax()) {
@@ -8122,6 +8589,9 @@ class Procuremen extends Backend
         $this->view->assign('sysRq', $isPurchaseConfirm
             ? $this->formatProcuremenSysRqForInput($this->resolveBundleSysRq(['pos' => $confirmBundle['pos'] ?? []]))
             : '');
+        $this->view->assign('deliveryDeadline', $isPurchaseConfirm
+            ? $this->resolveBundleDeliveryDeadline(['pos' => $confirmBundle['pos'] ?? []])
+            : '');
         $this->view->assign('requiredSidListJson', json_encode(array_values($requiredSidList), JSON_UNESCAPED_UNICODE));
         $this->view->assign('allDetailsBySidJson', json_encode($allDetailsBySid, JSON_UNESCAPED_UNICODE));
 

+ 1 - 0
application/admin/lang/zh-cn/customer.php

@@ -8,6 +8,7 @@ return [
     'Password'     => '密码',
     'Email'        => '邮箱',
     'Phone'        => '手机号',
+    'Score'        => '考核评分值',
     'Company_type' => '业务分类',
     'Createtime'   => '创建时间',
     'Updatetime'   => '更新时间',

+ 6 - 0
application/admin/view/customer/add.html

@@ -36,6 +36,12 @@
             <input id="c-password" class="form-control" name="row[password]" type="password" autocomplete="new-password" placeholder="留空则默认与手机号相同">
         </div>
     </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Score')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-score" class="form-control" name="row[score]" type="number" step="1" placeholder="选填">
+        </div>
+    </div>
     <div class="form-group company-type-form-group">
         <label class="control-label col-xs-12 col-sm-2">{:__('Company_type')}:</label>
         <div class="col-xs-12 col-sm-8">

+ 6 - 0
application/admin/view/customer/edit.html

@@ -36,6 +36,12 @@
             <input id="c-password" class="form-control" name="row[password]" type="password" autocomplete="new-password" placeholder="留空表示不修改密码">
         </div>
     </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Score')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-score" class="form-control" name="row[score]" type="number" step="1" value="{$row.score|htmlentities}" placeholder="选填">
+        </div>
+    </div>
     <div class="form-group company-type-form-group">
         <label class="control-label col-xs-12 col-sm-2">{:__('Company_type')}:</label>
         <div class="col-xs-12 col-sm-8">

+ 536 - 0
application/admin/view/procuremen/audit_append.html

@@ -0,0 +1,536 @@
+<style>
+    /*
+     * 补加供应商:与初选 review 同一套结构/样式(轻量弹窗壳下 body 直接包表单)
+     */
+    body.is-dialog > .review-dialog-form.audit-append-form,
+    .review-dialog-form.audit-append-form {
+        display: flex;
+        flex-direction: column;
+        height: 100% !important;
+        max-height: 100%;
+        min-height: 0;
+        box-sizing: border-box;
+        padding: 8px 12px 0;
+        overflow: hidden;
+        margin: 0;
+        background: #fff;
+    }
+    .audit-append-form .review-split {
+        display: flex;
+        flex-direction: column;
+        flex: 1 1 0%;
+        min-height: 0;
+        gap: 8px;
+        overflow: hidden;
+    }
+    .audit-append-form .review-split-left {
+        flex: 0 0 auto !important;
+        width: 100%;
+        min-width: 0;
+        height: auto !important;
+        padding-bottom: 8px;
+        border-bottom: 1px solid #e5e5e5;
+    }
+    .audit-append-form .review-merge-table-wrap {
+        max-height: 120px;
+        overflow: auto;
+        border: 1px solid #e5e5e5;
+    }
+    .audit-append-form .review-merge-table {
+        margin: 0;
+        font-size: 12px;
+    }
+    .audit-append-form .review-merge-table > thead > tr > th,
+    .audit-append-form .review-merge-table > tbody > tr > td {
+        padding: 3px 6px;
+        line-height: 1.35;
+    }
+    .audit-append-form .review-deadline-row {
+        display: flex;
+        flex-wrap: wrap;
+        align-items: center;
+        gap: 8px 20px;
+        margin-top: 8px;
+        padding-top: 8px;
+        border-top: 1px dashed #e0e0e0;
+        width: 100%;
+    }
+    .audit-append-form .review-deadline-item {
+        display: inline-flex !important;
+        flex-direction: row !important;
+        flex-wrap: nowrap !important;
+        align-items: center !important;
+        gap: 8px 10px;
+    }
+    .audit-append-form .review-deadline-row .review-field-label {
+        flex-shrink: 0;
+        margin: 0;
+        font-weight: 600;
+        font-size: 13px;
+        color: #000;
+    }
+    .audit-append-form .procuremen-sys-rq-split .procuremen-sys-rq-fields {
+        display: inline-flex !important;
+        flex-direction: row !important;
+        flex-wrap: nowrap !important;
+        align-items: center !important;
+        gap: 8px;
+        max-width: 100%;
+    }
+    .audit-append-form .procuremen-sys-rq-split .procuremen-sys-rq-time-group {
+        display: inline-flex !important;
+        flex-direction: row !important;
+        flex-wrap: nowrap !important;
+        align-items: center !important;
+        gap: 4px;
+        flex: 0 0 auto;
+    }
+    .audit-append-form .procuremen-sys-rq-date,
+    .audit-append-form .audit-delivery-deadline-input {
+        display: inline-block !important;
+        width: 148px !important;
+        min-width: 132px;
+        max-width: 148px;
+        height: 32px;
+        flex: 0 0 auto;
+    }
+    .audit-append-form .procuremen-sys-rq-hour,
+    .audit-append-form .procuremen-sys-rq-minute,
+    .audit-append-form select.form-control.procuremen-sys-rq-hour,
+    .audit-append-form select.form-control.procuremen-sys-rq-minute {
+        display: inline-block !important;
+        float: none !important;
+        width: 64px !important;
+        min-width: 64px !important;
+        max-width: 64px !important;
+        height: 32px !important;
+        padding: 4px 6px !important;
+        margin: 0 !important;
+        flex: 0 0 64px !important;
+        box-sizing: border-box !important;
+    }
+    .audit-append-form .procuremen-sys-rq-colon {
+        flex: 0 0 auto;
+        color: #666;
+        font-weight: 600;
+        line-height: 32px;
+    }
+    .audit-append-form .review-deadline-input-wrap {
+        display: inline-block;
+        flex: 0 0 auto;
+        vertical-align: middle;
+    }
+    .audit-append-form .review-selected-summary {
+        flex: 0 0 52px !important;
+        flex-grow: 0 !important;
+        flex-shrink: 0 !important;
+        flex-basis: 52px !important;
+        width: 100%;
+        margin: 0;
+        padding: 6px 8px;
+        background: #fafbfc;
+        border: 1px solid #e3e8ee;
+        border-radius: 3px;
+        font-size: 12px;
+        line-height: 1.4;
+        height: 52px !important;
+        min-height: 52px !important;
+        max-height: 52px !important;
+        overflow: hidden !important;
+        box-sizing: border-box !important;
+    }
+    .audit-append-form .review-selected-top {
+        display: flex;
+        flex-wrap: nowrap;
+        align-items: center;
+        gap: 10px 14px;
+        margin: 0 0 4px;
+        min-width: 0;
+        height: 18px;
+        overflow: hidden;
+    }
+    .audit-append-form .review-selected-head {
+        margin: 0;
+        font-weight: 600;
+        color: #333;
+        flex-shrink: 0;
+        white-space: nowrap;
+        line-height: 18px;
+    }
+    .audit-append-form .review-selected-count {
+        font-weight: 700;
+        color: #3c8dbc;
+    }
+    .audit-append-form .review-selected-tip {
+        margin: 0;
+        padding: 0;
+        border: none;
+        background: transparent;
+        color: #8a6d3b;
+        font-size: 12px;
+        line-height: 18px;
+        flex: 1 1 auto;
+        min-width: 0;
+        white-space: nowrap;
+        overflow: hidden;
+        text-overflow: ellipsis;
+    }
+    .audit-append-form .review-selected-tip .fa {
+        margin-right: 4px;
+        color: #f0ad4e;
+    }
+    /* 第二行固定槽位:empty / tags 叠放,勾选时不改变文档流高度 */
+    .audit-append-form .review-selected-slot {
+        position: relative;
+        height: 20px;
+        margin: 0;
+        overflow: hidden;
+    }
+    .audit-append-form .review-selected-empty {
+        position: absolute;
+        left: 0;
+        right: 0;
+        top: 0;
+        margin: 0;
+        color: #999;
+        font-size: 12px;
+        line-height: 20px;
+        white-space: nowrap;
+        overflow: hidden;
+        text-overflow: ellipsis;
+    }
+    .audit-append-form .review-selected-tags {
+        position: absolute;
+        left: 0;
+        right: 0;
+        top: 0;
+        display: flex;
+        flex-wrap: nowrap;
+        gap: 4px 6px;
+        align-items: center;
+        margin: 0;
+        height: 20px;
+        overflow: hidden;
+    }
+    .audit-append-form .review-selected-summary.is-empty .review-selected-tags {
+        visibility: hidden;
+        pointer-events: none;
+    }
+    .audit-append-form .review-selected-summary:not(.is-empty) .review-selected-empty {
+        visibility: hidden;
+        pointer-events: none;
+    }
+    .audit-append-form .review-selected-chip {
+        display: inline-flex;
+        align-items: center;
+        max-width: 100%;
+        padding: 0 4px 0 8px;
+        gap: 2px;
+        height: 20px;
+        font-size: 12px;
+        color: #2c6f9c;
+        background: #e8f4fb;
+        border: 1px solid #c5dce8;
+        border-radius: 2px;
+        box-sizing: border-box;
+    }
+    .audit-append-form .review-chip-text {
+        flex: 1 1 auto;
+        min-width: 0;
+        overflow: hidden;
+        text-overflow: ellipsis;
+        white-space: nowrap;
+        line-height: 18px;
+    }
+    .audit-append-form .review-chip-remove {
+        flex: 0 0 auto;
+        margin: 0;
+        padding: 0 2px;
+        border: none;
+        background: transparent;
+        color: #c9302c;
+        font-size: 15px;
+        font-weight: 700;
+        line-height: 1;
+        cursor: pointer;
+    }
+    .btn.procuremen-btn-slate {
+        color: #fff !important;
+        background-color: #4b5573 !important;
+        border-color: #3e4659 !important;
+    }
+    .btn.procuremen-btn-slate:hover,
+    .btn.procuremen-btn-slate:focus,
+    .btn.procuremen-btn-slate:active {
+        color: #fff !important;
+        background-color: #3f485f !important;
+        border-color: #353c4c !important;
+    }
+    .audit-append-form .review-split-right {
+        flex: 1 1 0% !important;
+        min-height: 0 !important;
+        height: auto !important;
+        max-height: none !important;
+        display: flex;
+        flex-direction: column;
+        overflow: hidden;
+    }
+    .audit-append-form .review-company-panel {
+        flex: 1 1 0% !important;
+        width: 100%;
+        min-height: 0 !important;
+        height: 100% !important;
+        max-height: none !important;
+        display: flex;
+        border: 1px solid #ddd;
+        border-radius: 3px;
+        background: #fff;
+        overflow: hidden;
+        box-sizing: border-box;
+    }
+    .audit-append-form .review-category-sidebar {
+        flex: 0 0 196px;
+        width: 196px;
+        min-width: 196px;
+        max-width: 196px;
+        border-right: 1px solid #e0e0e0;
+        background: #f9fafb;
+        overflow-y: auto;
+        font-size: 12px;
+    }
+    .audit-append-form .review-cat-head {
+        padding: 6px 8px;
+        font-weight: 600;
+        color: #000;
+        border-bottom: 1px solid #e5e5e5;
+        background: #f0f2f5;
+    }
+    .audit-append-form .review-cat-item {
+        display: block;
+        padding: 5px 8px;
+        color: #333;
+        border-bottom: 1px solid #eee;
+        cursor: pointer;
+        line-height: 1.35;
+        word-break: break-word;
+    }
+    .audit-append-form .review-cat-item:hover { background: #eef6fb; color: #2c6f9c; }
+    .audit-append-form .review-cat-item.active {
+        background: #3c8dbc;
+        color: #fff;
+        font-weight: 600;
+    }
+    .audit-append-form .review-cat-item .review-cat-count {
+        opacity: 0.85;
+        font-weight: normal;
+        font-size: 11px;
+    }
+    .audit-append-form .review-company-main {
+        flex: 1 1 0%;
+        min-width: 0;
+        min-height: 0;
+        display: flex;
+        flex-direction: column;
+        overflow: hidden;
+    }
+    .audit-append-form .review-company-toolbar {
+        flex: 0 0 auto;
+        padding: 6px 8px;
+        border-bottom: 1px solid #eee;
+        background: #fafafa;
+    }
+    .audit-append-form .review-company-toolbar .form-control {
+        font-size: 12px;
+        height: 30px;
+    }
+    .audit-append-form .review-table-scroll {
+        flex: 1 1 0% !important;
+        min-height: 0 !important;
+        overflow: auto !important;
+        font-size: 12px;
+        scrollbar-gutter: stable;
+    }
+    .audit-append-form .review-table-scroll .table-responsive {
+        margin-bottom: 0;
+        overflow: visible;
+    }
+    .audit-append-form .review-company-table {
+        margin-bottom: 0;
+        table-layout: fixed;
+        width: 100%;
+        border-collapse: separate;
+        border-spacing: 0;
+    }
+    .audit-append-form .review-company-table > thead > tr > th {
+        position: sticky;
+        top: 0;
+        z-index: 2;
+        background: #f5f5f5;
+    }
+    .audit-append-form .review-company-table > thead > tr > th,
+    .audit-append-form .review-company-table > tbody > tr > td {
+        padding: 3px 6px;
+        line-height: 1.35;
+        vertical-align: top;
+        word-break: break-word;
+        border: 1px solid #ddd;
+    }
+    .audit-append-form .review-company-table > thead > tr > th.review-th-cb,
+    .audit-append-form .review-company-table > tbody > tr > td.review-td-cb {
+        width: 34px !important;
+        min-width: 34px !important;
+        max-width: 34px !important;
+        text-align: center;
+        vertical-align: middle !important;
+        padding-left: 2px;
+        padding-right: 2px;
+    }
+    .audit-append-form .review-company-table .review-th-cb input[type="checkbox"],
+    .audit-append-form .review-company-table .review-td-cb input[type="checkbox"] {
+        margin: 0;
+        vertical-align: middle;
+        position: relative;
+        top: 1px;
+    }
+    .audit-append-form tr.is-existing td { color: #999; background: #f7f7f7; }
+    .audit-append-form .layer-footer {
+        flex: 0 0 auto;
+        width: 100%;
+        box-sizing: border-box;
+        margin: 0;
+        padding: 8px 14px 8px 0;
+        border-top: 1px solid #e5e5e5;
+        background: #fafafa;
+        text-align: right !important;
+    }
+    .audit-append-form .layer-footer .btn + .btn { margin-left: 8px; }
+</style>
+
+<form id="audit-append-form" class="review-dialog-form audit-append-form" role="form" style="margin:0;">
+    {:token()}
+    <input type="hidden" name="scydgy_id" id="audit-append-scydgy-id" value="{$scydgyId|htmlentities}"/>
+    <input type="hidden" id="audit-append-existing-json" value="{$existingCompanyNamesJson|default='[]'|htmlentities}"/>
+
+    <div class="review-split">
+        <div class="review-split-left">
+            <div class="review-merge-panel">
+                <div class="review-merge-table-wrap">
+                    <table class="table table-bordered table-condensed review-merge-table">
+                        <thead>
+                        <tr>
+                            <th class="text-center" style="width:100px;">订单号</th>
+                            <th style="min-width:160px;">印件名称</th>
+                            <th>工序名称</th>
+                            <th class="text-center" style="width:56px;">单位</th>
+                            <th class="text-center" style="width:72px;">工作量</th>
+                            <th class="text-center" style="width:72px;">本次数量</th>
+                            <th class="text-center" style="width:72px;">最高限价</th>
+                            <th class="text-center" style="width:80px;">订法</th>
+                            <th style="min-width:120px;">备注</th>
+                        </tr>
+                        </thead>
+                        <tbody>
+                        {volist name="processDisplayRows" id="pr"}
+                        <tr>
+                            {if $pr.show_order_cells}
+                            <td class="text-center"{if condition="$pr.order_rowspan gt 1"} rowspan="{$pr.order_rowspan}"{/if}>{$pr.CCYDH|default=''}</td>
+                            <td{if condition="$pr.order_rowspan gt 1"} rowspan="{$pr.order_rowspan}"{/if}>{$pr.CYJMC|default=''}</td>
+                            {/if}
+                            <td>{$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>
+                            <td>{$pr.MBZ|default=''}</td>
+                        </tr>
+                        {/volist}
+                        </tbody>
+                    </table>
+                </div>
+            </div>
+            <div class="review-deadline-row">
+                <div class="review-deadline-item">
+                    <span class="review-field-label">招标截止日期:</span>
+                    <div class="review-deadline-input-wrap procuremen-sys-rq-split is-readonly" id="audit-append-sys-rq-wrap" title="仅查看">
+                        <input type="hidden" id="audit-append-sys-rq" value="{$sysRq|default=''|htmlentities}"/>
+                        <div class="procuremen-sys-rq-fields" style="display:inline-flex;flex-direction:row;flex-wrap:nowrap;align-items:center;gap:8px;">
+                            <input type="date" class="form-control procuremen-sys-rq-date" title="招标截止日期" readonly="readonly" disabled="disabled" tabindex="-1" style="width:148px;height:32px;"/>
+                            <div class="procuremen-sys-rq-time-group" style="display:inline-flex;flex-direction:row;flex-wrap:nowrap;align-items:center;gap:4px;">
+                                <select class="form-control procuremen-sys-rq-hour" disabled="disabled" tabindex="-1" style="width:64px;height:32px;display:inline-block;"></select>
+                                <span class="procuremen-sys-rq-colon">:</span>
+                                <select class="form-control procuremen-sys-rq-minute" disabled="disabled" tabindex="-1" style="width:64px;height:32px;display:inline-block;"></select>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+                <div class="review-deadline-item">
+                    <span class="review-field-label">交货截止日期:</span>
+                    <input type="text" class="form-control audit-delivery-deadline-input" value="{$deliveryDeadline|default=''|htmlentities}" readonly="readonly" disabled="disabled" tabindex="-1"/>
+                </div>
+            </div>
+        </div>
+
+        <div class="review-selected-summary is-empty" id="review-selected-summary" aria-live="polite">
+            <div class="review-selected-top">
+                <div class="review-selected-head">
+                    已选单位(<span class="review-selected-count" id="review-selected-count">0</span>)
+                </div>
+                <p class="review-selected-tip" role="note">
+                    <i class="fa fa-exclamation-triangle"></i>
+                    <strong>重要提示:</strong>确认后将向新选供应商<strong>发送短信与邮件</strong>;已下发过的供应商不可再选。
+                </p>
+            </div>
+            <div class="review-selected-slot">
+                <p class="review-selected-empty" id="review-selected-empty">暂未选择供应商</p>
+                <div class="review-selected-tags" id="review-selected-tags"></div>
+            </div>
+        </div>
+
+        <div class="review-split-right">
+            <div class="review-company-panel">
+                <aside class="review-category-sidebar" id="review-category-sidebar">
+                    <div class="review-cat-head">业务分组</div>
+                    <div id="review-category-list"></div>
+                </aside>
+                <div class="review-company-main">
+                    <div class="review-company-toolbar">
+                        <input type="text" class="form-control" id="review-company-search" placeholder="搜索供应商名称、姓名、邮箱、手机号、业务分类…" autocomplete="off"/>
+                    </div>
+                    <div class="review-table-scroll">
+                        <div class="table-responsive">
+                            <table class="table table-bordered table-hover table-striped table-condensed review-company-table">
+                                <colgroup>
+                                    <col style="width:34px"/>
+                                    <col style="width:27%"/>
+                                    <col style="width:9%"/>
+                                    <col style="width:16%"/>
+                                    <col style="width:15%"/>
+                                    <col style="width:27%"/>
+                                </colgroup>
+                                <thead>
+                                <tr>
+                                    <th class="review-th-cb"><input type="checkbox" id="review-check-all"/></th>
+                                    <th>供应商名称</th>
+                                    <th>姓名</th>
+                                    <th>邮箱</th>
+                                    <th>手机号</th>
+                                    <th>业务分类</th>
+                                </tr>
+                                </thead>
+                                <tbody id="review-company-tbody"></tbody>
+                            </table>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+
+    <div class="layer-footer clearfix">
+        <div class="pull-right">
+            <button type="button" style="margin-right:20px" id="btn-audit-append-submit" class="btn procuremen-btn-slate btn-embossed">确认</button>
+            <button type="button" style="margin-right:20px" class="btn btn-default btn-embossed btn-close" id="btn-audit-append-close">{:__('Close')}</button>
+        </div>
+    </div>
+</form>

+ 75 - 28
application/admin/view/procuremen/audit_issue.html

@@ -102,13 +102,19 @@
         display: flex;
         align-items: flex-start;
         flex-wrap: wrap;
-        gap: 8px 12px;
+        gap: 10px 20px;
         margin: 0 0 12px;
         font-size: 13px;
         position: relative;
         z-index: 61;
         overflow: visible;
     }
+    .audit-issue-wrap .audit-deadline-item {
+        display: flex;
+        flex-wrap: wrap;
+        align-items: flex-start;
+        gap: 8px 10px;
+    }
     .audit-issue-wrap .audit-deadline-row .audit-field-label {
         font-weight: 600;
         color: #333;
@@ -122,6 +128,13 @@
         min-width: 0;
         z-index: 62;
     }
+    .audit-issue-wrap .audit-delivery-deadline-input {
+        width: 148px;
+        min-width: 132px;
+        height: 32px;
+        padding: 4px 8px;
+        margin-top: 0;
+    }
     .audit-issue-wrap .procuremen-sys-rq-split .procuremen-sys-rq-fields {
         display: flex;
         flex-wrap: wrap;
@@ -172,16 +185,26 @@
         overflow-y: visible;
     }
     .audit-issue-wrap .audit-quote-detail-table {
-        min-width: 1320px;
+        min-width: 1580px;
     }
     .audit-issue-wrap .audit-quote-detail-table td.audit-quote-empty {
         color: #e67e22;
     }
+    .audit-issue-wrap .audit-deadline-item.audit-bid-open-item {
+        align-items: center;
+        margin-top: 0;
+    }
+    .audit-issue-wrap .audit-deadline-row .audit-append-supplier-btn {
+        flex-shrink: 0;
+        height: 32px;
+        line-height: 20px;
+        margin-right: 8px;
+    }
     .audit-issue-wrap .audit-deadline-row .audit-bid-open-btn {
         flex-shrink: 0;
         height: 32px;
         line-height: 20px;
-        margin-left: 4px;
+        margin-left: 0;
     }
     .audit-issue-wrap .audit-deadline-row .audit-bid-open-verified-tag {
         flex-shrink: 0;
@@ -191,7 +214,7 @@
         font-weight: 600;
         color: #3c763d;
         white-space: nowrap;
-        margin-left: 8px;
+        margin-left: 0;
     }
     .audit-issue-wrap .audit-deadline-row .audit-bid-open-verified-tag .fa {
         font-size: 16px;
@@ -225,7 +248,7 @@
         color: #333;
     }
     .audit-bid-open-modal .bid-open-row select {
-        width: 180px;
+        width: 260px;
         height: 32px;
     }
     .audit-bid-open-modal .bid-open-row .bid-open-code {
@@ -237,6 +260,8 @@
     }
     .audit-issue-wrap .audit-quote-detail-table td.audit-quote-pending {
         color: #e67e22;
+        white-space: nowrap;
+        min-width: 130px;
     }
 </style>
 <div class="audit-issue-wrap">
@@ -290,29 +315,45 @@
         <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-append-deadline-passed" value="{$appendSupplierDeadlinePassed|default=0}"/>
         <input type="hidden" id="audit-order-ccydh" value="{$orderCcydh|default=''|htmlentities}"/>
         <div class="audit-deadline-row">
-            <span class="audit-field-label">截止时间:</span>
-            <div class="audit-deadline-input-wrap procuremen-sys-rq-split is-readonly" id="audit-sys-rq-wrap" title="仅查看,不可修改">
-                <input type="hidden" id="audit-sys-rq" name="sys_rq" value="{$sysRq|default=''|htmlentities}"/>
-                <div class="procuremen-sys-rq-fields">
-                    <input type="date" class="form-control procuremen-sys-rq-date" title="截止日期" readonly="readonly" disabled="disabled" tabindex="-1"/>
-                    <div class="procuremen-sys-rq-time-group">
-                        <select class="form-control procuremen-sys-rq-hour" title="时" disabled="disabled" tabindex="-1"></select>
-                        <span class="procuremen-sys-rq-colon">:</span>
-                        <select class="form-control procuremen-sys-rq-minute" title="分" disabled="disabled" tabindex="-1"></select>
+            <div class="audit-deadline-item">
+                <span class="audit-field-label">招标截止日期:</span>
+                <div class="audit-deadline-input-wrap procuremen-sys-rq-split is-readonly" id="audit-sys-rq-wrap" title="仅查看,不可修改">
+                    <input type="hidden" id="audit-sys-rq" name="sys_rq" value="{$sysRq|default=''|htmlentities}"/>
+                    <div class="procuremen-sys-rq-fields">
+                        <input type="date" class="form-control procuremen-sys-rq-date" title="招标截止日期" readonly="readonly" disabled="disabled" tabindex="-1"/>
+                        <div class="procuremen-sys-rq-time-group">
+                            <select class="form-control procuremen-sys-rq-hour" title="时" disabled="disabled" tabindex="-1"></select>
+                            <span class="procuremen-sys-rq-colon">:</span>
+                            <select class="form-control procuremen-sys-rq-minute" title="分" disabled="disabled" tabindex="-1"></select>
+                        </div>
                     </div>
-                    {if $bidOpenVerified}
-                    <span class="audit-bid-open-verified-tag"><i class="fa fa-check-circle"></i> 已开标验证</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">
-                        <i class="fa fa-unlock-alt"></i> 开标
-                    </button>
-                    {/if}
-                    {/if}
                 </div>
             </div>
+            <div class="audit-deadline-item">
+                <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>
+            <div class="audit-deadline-item audit-bid-open-item">
+                {if $canAppendSupplier}
+                {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> 补加供应商
+                </button>
+                {/if}
+                {/if}
+                {if $bidOpenVerified}
+                <span class="audit-bid-open-verified-tag"><i class="fa fa-check-circle"></i> 已开标验证</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">
+                    <i class="fa fa-unlock-alt"></i> 开标
+                </button>
+                {/if}
+                {/if}
+            </div>
         </div>
     </form>
 <!--    <p class="audit-notify-tip" style="margin-top:-2px;">-->
@@ -331,12 +372,14 @@
                 <th class="text-center" style="width:42px;">选定</th>
                 <th style="min-width:140px;">供应商名称</th>
                 <th style="width:88px;">姓名</th>
-                <th style="min-width:160px;">邮箱</th>
+                <th style="width:100px;">邮箱</th>
                 <th style="width:120px;">手机号</th>
                 <th style="width:130px;">工序名称</th>
-                <th class="text-center" style="width:112px;">单价</th>
-                <th class="text-center" style="width:118px;">交货日期</th>
-                <th class="text-center" style="width:100px;">小计</th>
+                <th class="text-center" style="width:130px;">单价</th>
+                <th class="text-center" style="width:130px;">预估工期</th>
+                <th class="text-center" style="width:130px;">交货日期</th>
+                <th class="text-center" style="width:130px;">小计</th>
+                <th class="text-center" style="width:160px;">操作时间</th>
                 <th class="text-center" style="width:120px;">操作</th>
             </tr>
             </thead>
@@ -358,8 +401,10 @@
                 {/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>
+                <td class="text-center{if condition="!empty($ln.lead_days_quote_pending)"} audit-quote-pending{elseif condition="$ln.lead_days_filled neq 1"} audit-quote-empty{/if}">{$ln.lead_days_show|default='未填写'|htmlentities}</td>
                 <td class="text-center{if condition="!empty($ln.delivery_quote_pending)"} audit-quote-pending{elseif condition="$ln.delivery_filled neq 1"} audit-quote-empty{/if}">{$ln.delivery_show|default='未填写'|htmlentities}</td>
                 <td class="text-center{if condition="!empty($ln.amount_quote_pending)"} audit-quote-pending{/if}">{$ln.subtotal_text|default=''|htmlentities}</td>
+                <td class="text-center{if condition="!empty($ln.oper_time_quote_pending)"} audit-quote-pending{/if}">{$ln.oper_time_text|default=''|htmlentities}</td>
                 {if $lk == 1}
                 <td class="text-center audit-op-cell"{if condition="$co.display_rowspan gt 1"} rowspan="{$co.display_rowspan}"{/if} style="vertical-align:middle;">
                     {if $auth->check('procuremen/auditresendemail')}
@@ -375,7 +420,7 @@
             {if !empty($co.has_remark)}
             <tr{if !empty($co.is_void)} class="text-muted"{/if}>
                 <td><strong>备注</strong></td>
-                <td colspan="3" class="{if !empty($co.remark_quote_pending)}audit-quote-pending{/if}" style="text-align:left;vertical-align:middle;">{$co.remark|default=''|htmlentities}</td>
+                <td colspan="5" class="{if !empty($co.remark_quote_pending)}audit-quote-pending{/if}" style="text-align:left;vertical-align:middle;">{$co.remark|default=''|htmlentities}</td>
             </tr>
             {/if}
             {if !empty($co.has_total)}
@@ -383,7 +428,9 @@
                 <td><strong>总计</strong></td>
                 <td></td>
                 <td></td>
+                <td></td>
                 <td class="text-center"><strong>{$co.total_text|default=''|htmlentities}</strong></td>
+                <td></td>
             </tr>
             {/if}
             {/volist}

+ 41 - 15
application/admin/view/procuremen/details_fragment.html

@@ -226,7 +226,7 @@
     .procuremen-details-wrap .detail-supplier-table {
         table-layout: fixed;
         width: 100%;
-        min-width: 1100px;
+        min-width: 1320px;
         border-collapse: collapse;
     }
     .procuremen-details-wrap .detail-supplier-table th.col-supplier-name,
@@ -237,7 +237,7 @@
     }
     .procuremen-details-wrap .detail-supplier-table th.col-supplier-email,
     .procuremen-details-wrap .detail-supplier-table td.col-supplier-email {
-        width: 110px;
+        width: 100px;
         word-break: break-all;
     }
     .procuremen-details-wrap .detail-supplier-table th.col-supplier-user,
@@ -251,12 +251,27 @@
     }
     .procuremen-details-wrap .detail-supplier-table th.col-unit-price,
     .procuremen-details-wrap .detail-supplier-table td.col-unit-price {
-        width: 112px;
+        width: 130px;
+        white-space: nowrap;
+    }
+    .procuremen-details-wrap .detail-supplier-table th.col-lead-days,
+    .procuremen-details-wrap .detail-supplier-table td.col-lead-days {
+        width: 130px;
+        white-space: nowrap;
+    }
+    .procuremen-details-wrap .detail-supplier-table th.col-delivery,
+    .procuremen-details-wrap .detail-supplier-table td.col-delivery {
+        width: 130px;
+        white-space: nowrap;
+    }
+    .procuremen-details-wrap .detail-supplier-table th.col-subtotal,
+    .procuremen-details-wrap .detail-supplier-table td.col-subtotal {
+        width: 130px;
         white-space: nowrap;
     }
     .procuremen-details-wrap .detail-supplier-table th.col-op-time,
     .procuremen-details-wrap .detail-supplier-table td.col-op-time {
-        width: 148px;
+        width: 160px;
         white-space: nowrap;
     }
     .procuremen-details-wrap .detail-supplier-table > thead > tr > th,
@@ -272,6 +287,7 @@
     .procuremen-details-wrap .detail-supplier-table td.detail-quote-empty,
     .procuremen-details-wrap .detail-supplier-table td.detail-quote-pending {
         color: #e67e22 !important;
+        white-space: nowrap;
     }
     .procuremen-details-wrap .detail-supplier-remark-row td {
         background: #f5f5f5;
@@ -445,14 +461,15 @@
             <tr>
                 <th class="col-supplier-name">供应商名称</th>
                 <th class="col-supplier-user" style="width:72px;">姓名</th>
-                <th class="col-supplier-email" style="width:110px;">邮箱</th>
+                <th class="col-supplier-email" style="width:100px;">邮箱</th>
                 <th class="col-supplier-phone" style="width:120px;">手机号</th>
                 <th style="width:120px;">工序名称</th>
-                <th class="text-center col-unit-price" style="width:112px;">单价</th>
-                <th class="text-center" style="width:100px;">交货日期</th>
-                <th class="text-center" style="width:88px;">小计</th>
+                <th class="text-center col-unit-price" style="width:130px;">单价</th>
+                <th class="text-center col-lead-days" style="width:130px;">预估工期</th>
+                <th class="text-center col-delivery" style="width:130px;">交货日期</th>
+                <th class="text-center col-subtotal" style="width:130px;">小计</th>
                 <th class="text-center" style="width:88px;">状态</th>
-                <th class="col-op-time" style="width:148px;">操作时间</th>
+                <th class="col-op-time" style="width:160px;">操作时间</th>
             </tr>
             </thead>
             <tbody>
@@ -470,8 +487,9 @@
                 {/if}
                 <td>{$ln.cgymc|default=''|htmlentities}</td>
                 <td class="text-center col-unit-price{if condition="!empty($ln.amount_quote_pending)"} detail-quote-pending{elseif condition="empty($ln.amount_filled) || $ln.amount_filled neq 1"} detail-quote-empty{/if}">{$ln.unit_price_text|default='未填写'|htmlentities}</td>
-                <td class="text-center{if condition="!empty($ln.delivery_quote_pending)"} detail-quote-pending{elseif condition="isset($ln.delivery_filled) && $ln.delivery_filled neq 1"} detail-quote-empty{/if}">{$ln.delivery_show|default=''|htmlentities}</td>
-                <td class="text-center{if condition="!empty($ln.amount_quote_pending)"} detail-quote-pending{/if}">{$ln.subtotal_text|default=''|htmlentities}</td>
+                <td class="text-center col-lead-days{if condition="!empty($ln.lead_days_quote_pending)"} detail-quote-pending{elseif condition="isset($ln.lead_days_filled) && $ln.lead_days_filled neq 1"} detail-quote-empty{/if}">{$ln.lead_days_show|default='未填写'|htmlentities}</td>
+                <td class="text-center col-delivery{if condition="!empty($ln.delivery_quote_pending)"} detail-quote-pending{elseif condition="isset($ln.delivery_filled) && $ln.delivery_filled neq 1"} detail-quote-empty{/if}">{$ln.delivery_show|default=''|htmlentities}</td>
+                <td class="text-center col-subtotal{if condition="!empty($ln.amount_quote_pending)"} detail-quote-pending{/if}">{$ln.subtotal_text|default=''|htmlentities}</td>
                 <td class="text-center">
                     {if !empty($ln.is_void)}
                     <span class="label label-default">历史</span>
@@ -483,13 +501,13 @@
                     <span class="text-muted"> </span>
                     {/if}
                 </td>
-                <td class="col-op-time">{$ln.oper_time_text|default=''|htmlentities}</td>
+                <td class="col-op-time{if condition="!empty($ln.oper_time_quote_pending)"} detail-quote-pending{/if}">{$ln.oper_time_text|default=''|htmlentities}</td>
             </tr>
             {/volist}
             {if !empty($sg.has_remark)}
             <tr class="detail-supplier-remark-row{if !empty($sg.is_void)} text-muted{/if}">
                 <td class="detail-remark-label"><strong>备注</strong></td>
-                <td class="detail-remark-text{if !empty($sg.remark_quote_pending)} detail-quote-pending{/if}" colspan="5">{$sg.remark|default=''|htmlentities}</td>
+                <td class="detail-remark-text{if !empty($sg.remark_quote_pending)} detail-quote-pending{/if}" colspan="6">{$sg.remark|default=''|htmlentities}</td>
             </tr>
             {/if}
             {if !empty($sg.has_total)}
@@ -497,6 +515,7 @@
                 <td><strong>总计</strong></td>
                 <td></td>
                 <td></td>
+                <td></td>
                 <td class="text-center"><strong>{$sg.total_text|default=''|htmlentities}</strong></td>
                 <td colspan="2"></td>
             </tr>
@@ -518,6 +537,13 @@
                     <span class="detail-quote-empty">未填写</span>
                     {/if}
                 </td>
+                <td class="text-center">
+                    {if condition="isset($dr.lead_days) && $dr.lead_days !== '' && $dr.lead_days !== null"}
+                    {$dr.lead_days|htmlentities}天
+                    {else /}
+                    <span class="detail-quote-empty">未填写</span>
+                    {/if}
+                </td>
                 <td class="text-center">
                     {if condition="$dr.delivery_ymd !== '' && $dr.delivery_ymd !== null"}
                     {$dr.delivery_ymd|htmlentities}
@@ -537,12 +563,12 @@
                     <span class="text-muted"> </span>
                     {/if}
                 </td>
-                <td class="col-op-time">{$dr.oper_time_text|default=''|htmlentities}</td>
+                <td class="col-op-time{if condition="!empty($dr.oper_time_quote_pending)"} detail-quote-pending{/if}">{$dr.oper_time_text|default=''|htmlentities}</td>
             </tr>
             {/volist}
             {else /}
             <tr>
-                <td colspan="10" class="text-center text-muted">暂无下发明细</td>
+                <td colspan="11" class="text-center text-muted">暂无下发明细</td>
             </tr>
             {/notempty}
             {/notempty}

+ 21 - 0
application/admin/view/procuremen/dialog_lite_shell.html

@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<html style="height:100%;">
+<head>
+{include file="common/meta" /}
+<title>{$dialogTitle|default='选商'|htmlentities}</title>
+<style>
+    html, body {
+        height: 100% !important;
+        max-height: 100% !important;
+        margin: 0 !important;
+        padding: 0 !important;
+        overflow: hidden !important;
+        background: #fff;
+    }
+</style>
+</head>
+<body class="inside-header inside-aside is-dialog" style="height:100%;overflow:hidden;">
+{:isset($dialogBody) ? $dialogBody : ''}
+{include file="common/script" /}
+</body>
+</html>

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

@@ -119,16 +119,29 @@
         display: flex;
         align-items: center;
         flex-wrap: wrap;
-        gap: 8px 12px;
+        gap: 10px 20px;
         margin: 0 0 10px;
         font-size: 13px;
     }
+    .outward-confirm-deadline-item {
+        display: flex;
+        flex-wrap: wrap;
+        align-items: center;
+        gap: 8px 10px;
+    }
     .outward-confirm-deadline-row .deadline-label {
         font-weight: 600;
         color: #333;
         white-space: nowrap;
         margin: 0;
     }
+    .outward-confirm-delivery-deadline-input {
+        width: 148px;
+        min-width: 132px;
+        height: 32px;
+        padding: 4px 8px;
+        font-size: 13px;
+    }
     .outward-confirm-deadline-wrap.procuremen-sys-rq-split .procuremen-sys-rq-fields {
         display: flex;
         flex-wrap: wrap;
@@ -287,18 +300,24 @@
     {/if}
 
     <div class="outward-confirm-deadline-row">
-        <label class="deadline-label">截止时间:</label>
-        <div class="outward-confirm-deadline-wrap procuremen-sys-rq-split is-readonly" id="outward-confirm-sys-rq-wrap" title="仅查看,不可修改">
-            <input type="hidden" id="outward-confirm-sys-rq" name="sys_rq" value="{$sysRq|default=''|htmlentities}"/>
-            <div class="procuremen-sys-rq-fields">
-                <input type="date" class="form-control procuremen-sys-rq-date" title="截止日期" readonly="readonly" disabled="disabled" tabindex="-1"/>
-                <div class="procuremen-sys-rq-time-group">
-                    <select class="form-control procuremen-sys-rq-hour" title="时" disabled="disabled" tabindex="-1"></select>
-                    <span class="procuremen-sys-rq-colon">:</span>
-                    <select class="form-control procuremen-sys-rq-minute" title="分" disabled="disabled" tabindex="-1"></select>
+        <div class="outward-confirm-deadline-item">
+            <label class="deadline-label">招标截止日期:</label>
+            <div class="outward-confirm-deadline-wrap procuremen-sys-rq-split is-readonly" id="outward-confirm-sys-rq-wrap" title="仅查看,不可修改">
+                <input type="hidden" id="outward-confirm-sys-rq" name="sys_rq" value="{$sysRq|default=''|htmlentities}"/>
+                <div class="procuremen-sys-rq-fields">
+                    <input type="date" class="form-control procuremen-sys-rq-date" title="招标截止日期" readonly="readonly" disabled="disabled" tabindex="-1"/>
+                    <div class="procuremen-sys-rq-time-group">
+                        <select class="form-control procuremen-sys-rq-hour" title="时" disabled="disabled" tabindex="-1"></select>
+                        <span class="procuremen-sys-rq-colon">:</span>
+                        <select class="form-control procuremen-sys-rq-minute" title="分" disabled="disabled" tabindex="-1"></select>
+                    </div>
                 </div>
             </div>
         </div>
+        <div class="outward-confirm-deadline-item">
+            <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>
     </div>
 
     <p class="audit-notify-tip">
@@ -316,8 +335,10 @@
                 <th style="width:120px;">手机号</th>
                 <th style="width:120px;">工序名称</th>
                 <th class="text-center" style="width:112px;">单价</th>
+                <th class="text-center" style="width:88px;">预估工期</th>
                 <th class="text-center" style="width:118px;">交货日期</th>
                 <th class="text-center" style="width:100px;">小计</th>
+                <th class="text-center" style="width:148px;">操作时间</th>
             </tr>
             </thead>
             <tbody>
@@ -339,14 +360,16 @@
                 {/if}
                 <td class="outward-process-name">{$ql.cgymc|default=''|htmlentities}</td>
                 <td class="text-center{if condition="!empty($ql.amount_quote_pending)"} outward-quote-empty{elseif condition="$ql.amount_filled neq 1"} outward-quote-empty{/if}">{$ql.unit_price_text|default='未填写'|htmlentities}</td>
+                <td class="text-center{if condition="!empty($ql.lead_days_quote_pending)"} outward-quote-empty{elseif condition="$ql.lead_days_filled neq 1"} outward-quote-empty{/if}">{$ql.lead_days_text|default='未填写'|htmlentities}</td>
                 <td class="text-center{if condition="!empty($ql.delivery_quote_pending)"} outward-quote-empty{elseif condition="$ql.delivery_filled neq 1"} outward-quote-empty{/if}">{$ql.delivery_text|default='未填写'|htmlentities}</td>
                 <td class="text-center">{$ql.subtotal_text|default=''|htmlentities}</td>
+                <td class="text-center">{$ql.oper_time_text|default=''|htmlentities}</td>
             </tr>
             {/volist}
             {if !empty($co.has_remark)}
             <tr class="outward-confirm-supplier-row{if !empty($co.is_void)} text-muted{/if}" data-supplier-idx="{$k-1}">
                 <td><strong>备注</strong></td>
-                <td colspan="3" class="{if !empty($co.remark_quote_pending)}outward-quote-empty{/if}" style="text-align:left;vertical-align:middle;">{$co.remark|default=''|htmlentities}</td>
+                <td colspan="5" class="{if !empty($co.remark_quote_pending)}outward-quote-empty{/if}" style="text-align:left;vertical-align:middle;">{$co.remark|default=''|htmlentities}</td>
             </tr>
             {/if}
             {if !empty($co.has_total)}
@@ -354,13 +377,15 @@
                 <td><strong>总计</strong></td>
                 <td></td>
                 <td></td>
+                <td></td>
                 <td class="text-center"><strong>{$co.total_text|default=''|htmlentities}</strong></td>
+                <td></td>
             </tr>
             {/if}
             {/volist}
             {empty name="confirmPickGroups"}
             <tr>
-                <td colspan="9" class="text-center text-muted">暂无供应商报价</td>
+                <td colspan="11" class="text-center text-muted">暂无供应商报价</td>
             </tr>
             {/empty}
             </tbody>

+ 100 - 45
application/admin/view/procuremen/review.html

@@ -1,58 +1,70 @@
 <style>
     /*
-     * 上:订单信息(订单号、印件名称突出);下:左侧分类 + 右侧搜索与公司表格。
+     * 初选选商:与补加同一套轻量弹窗壳 + flex 列表高度
      */
+    html, body.is-dialog {
+        height: 100% !important;
+        overflow: hidden !important;
+        margin: 0 !important;
+    }
     body.is-dialog #main,
     body.is-dialog #main > .tab-content,
     body.is-dialog #main #content {
-        height: 100%;
+        height: 100% !important;
         min-height: 0;
+        overflow: hidden;
     }
     body.is-dialog #content > .row,
     body.is-dialog #content > .row > [class*="col-"] {
-        height: 100%;
+        height: 100% !important;
         min-height: 0;
     }
     body.is-dialog .content {
-        height: 100%;
+        height: 100% !important;
         min-height: 0;
         box-sizing: border-box;
+        padding: 0 !important;
+        overflow: hidden;
     }
+    body.is-dialog > .review-dialog-form,
     body.is-dialog .review-dialog-form,
     .review-dialog-form {
         display: flex;
         flex-direction: column;
-        height: 100%;
+        height: 100% !important;
         max-height: 100%;
-        min-height: min(72vh, 640px);
+        min-height: 0;
         box-sizing: border-box;
         padding: 8px 12px 0;
+        overflow: hidden;
+        margin: 0;
+        background: #fff;
     }
     .review-split {
         display: flex;
         flex-direction: column;
         align-items: stretch;
         flex-wrap: nowrap;
-        gap: 10px;
+        gap: 8px;
         flex: 1 1 0%;
         min-height: 0;
-        overflow: visible;
+        overflow: hidden;
     }
     .review-split-left {
         flex: 0 0 auto;
         width: 100%;
         min-width: 0;
-        padding-bottom: 10px;
+        padding-bottom: 8px;
         border-bottom: 1px solid #e5e5e5;
         overflow: visible;
         position: relative;
         z-index: 60;
     }
     .review-split-right {
-        flex: 1 1 0%;
+        flex: 1 1 0% !important;
         width: 100%;
         min-width: 0;
-        min-height: 0;
+        min-height: 0 !important;
         display: flex;
         flex-direction: column;
         overflow: hidden;
@@ -129,7 +141,7 @@
         display: flex;
         flex-wrap: wrap;
         align-items: flex-start;
-        gap: 8px 12px;
+        gap: 10px 20px;
         margin-top: 10px;
         padding-top: 10px;
         border-top: 1px dashed #e0e0e0;
@@ -138,6 +150,14 @@
         z-index: 61;
         overflow: visible;
     }
+    .review-deadline-item {
+        display: flex;
+        flex-wrap: wrap;
+        align-items: center;
+        gap: 8px 10px;
+        flex: 0 1 auto;
+        min-width: 0;
+    }
     .review-deadline-row .review-field-label {
         flex-shrink: 0;
         margin: 0;
@@ -147,10 +167,28 @@
     }
     .review-deadline-input-wrap {
         position: relative;
-        flex: 1 1 auto;
+        flex: 0 1 auto;
         min-width: 0;
         z-index: 62;
     }
+    .review-delivery-deadline-input {
+        width: 148px;
+        min-width: 132px;
+        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,
+    .review-delivery-deadline-input:not(.has-value)::-webkit-datetime-edit-year-field,
+    .review-delivery-deadline-input:not(.has-value)::-webkit-datetime-edit-month-field,
+    .review-delivery-deadline-input:not(.has-value)::-webkit-datetime-edit-day-field {
+        color: transparent;
+        opacity: 0;
+    }
+    .review-delivery-deadline-input.has-value {
+        color: #333;
+    }
     .procuremen-sys-rq-split .procuremen-sys-rq-fields {
         display: flex;
         flex-wrap: wrap;
@@ -222,10 +260,13 @@
         background-color: #3f485f !important;
         border-color: #353c4c !important;
     }
-    /* 左侧分类 + 右侧列表 */
+    /* 供应商列表:flex 占满剩余高度(与补加一致),内部滚动 */
     .review-company-panel {
-        flex: 1 1 0%;
-        min-height: 0;
+        flex: 1 1 0% !important;
+        width: 100%;
+        height: auto !important;
+        min-height: 0 !important;
+        max-height: none !important;
         display: flex;
         flex-direction: row;
         align-items: stretch;
@@ -234,12 +275,13 @@
         border-radius: 3px;
         background: #fff;
         overflow: hidden;
+        box-sizing: border-box;
     }
     .review-category-sidebar {
-        flex: 0 0 210px;
-        width: 210px;
-        min-width: 210px;
-        max-width: 210px;
+        flex: 0 0 196px;
+        width: 196px;
+        min-width: 196px;
+        max-width: 196px;
         border-right: 1px solid #e0e0e0;
         background: #f9fafb;
         overflow-y: auto;
@@ -247,7 +289,7 @@
         font-size: 12px;
     }
     .review-category-sidebar .review-cat-head {
-        padding: 8px 10px;
+        padding: 6px 8px;
         font-weight: 600;
         color: #000;
         border-bottom: 1px solid #e5e5e5;
@@ -255,7 +297,7 @@
     }
     .review-category-sidebar .review-cat-item {
         display: block;
-        padding: 8px 10px;
+        padding: 5px 8px;
         color: #333;
         text-decoration: none;
         border-bottom: 1px solid #eee;
@@ -263,7 +305,7 @@
         white-space: normal;
         word-break: break-word;
         overflow: visible;
-        line-height: 1.45;
+        line-height: 1.35;
     }
     .review-category-sidebar .review-cat-item:hover {
         background: #eef6fb;
@@ -292,17 +334,18 @@
     }
     .review-company-toolbar {
         flex-shrink: 0;
-        padding: 8px;
+        padding: 6px 8px;
         border-bottom: 1px solid #eee;
         background: #fafafa;
     }
     .review-company-toolbar .form-control {
-        font-size: 13px;
-        height: 34px;
+        font-size: 12px;
+        height: 30px;
     }
     .review-table-scroll {
-        flex: 1 1 0%;
-        min-height: 0;
+        flex: 1 1 0% !important;
+        min-height: 0 !important;
+        height: auto !important;
         box-sizing: border-box;
         width: 100%;
         overflow: auto;
@@ -319,7 +362,8 @@
     }
     .review-table-scroll .table > thead > tr > th,
     .review-table-scroll .table > tbody > tr > td {
-        padding: 5px 6px;
+        padding: 3px 6px;
+        line-height: 1.35;
     }
     /* 客户列表滚动时表头固定在滚动区域顶部 */
     .review-table-scroll .review-company-table > thead > tr > th {
@@ -393,8 +437,8 @@
         flex-shrink: 0;
         width: 100%;
         box-sizing: border-box;
-        margin-top: 12px;
-        padding: 10px 14px 10px 0;
+        margin-top: 0;
+        padding: 8px 14px 8px 0;
         border-top: 1px solid #e5e5e5;
         background: #fafafa;
         text-align: right !important;
@@ -405,16 +449,21 @@
     .review-dialog-form .layer-footer .btn + .btn {
         margin-left: 8px;
     }
-    /* 已选单位:介于订单信息与下方提示之间,筛选滚动时仍可看到 */
+    /* 已选单位:高度封顶,避免选中后把下方列表顶矮 */
     .review-selected-summary {
         flex-shrink: 0;
-        margin: 0 0 10px;
-        padding: 8px 10px;
+        margin: 0 0 8px;
+        padding: 6px 8px;
         background: #fafbfc;
         border: 1px solid #e3e8ee;
         border-radius: 3px;
         font-size: 12px;
-        line-height: 1.45;
+        line-height: 1.4;
+        height: 52px;
+        min-height: 52px;
+        max-height: 52px;
+        overflow: hidden;
+        box-sizing: border-box;
     }
     .review-selected-summary .review-selected-top {
         display: flex;
@@ -499,7 +548,7 @@
         color: #000;
     }
     .review-merge-table-wrap {
-        max-height: 200px;
+        max-height: 120px;
         overflow: auto;
         border: 1px solid #e5e5e5;
     }
@@ -544,18 +593,24 @@
                 </div>
             </div>
             <div class="review-deadline-row">
-                <span class="review-field-label">截止时间:<span class="text-danger">*</span></span>
-                <div class="review-deadline-input-wrap procuremen-sys-rq-split" id="review-sys-rq-wrap">
-                    <input type="hidden" id="review-sys-rq" name="sys_rq" value=""/>
-                    <div class="procuremen-sys-rq-fields">
-                        <input type="date" class="form-control procuremen-sys-rq-date" title="截止日期"/>
-                        <div class="procuremen-sys-rq-time-group">
-                            <select class="form-control procuremen-sys-rq-hour" title="时"></select>
-                            <span class="procuremen-sys-rq-colon">:</span>
-                            <select class="form-control procuremen-sys-rq-minute" title="分"></select>
+                <div class="review-deadline-item">
+                    <span class="review-field-label">招标截止日期:<span class="text-danger">*</span></span>
+                    <div class="review-deadline-input-wrap procuremen-sys-rq-split" id="review-sys-rq-wrap">
+                        <input type="hidden" id="review-sys-rq" name="sys_rq" value=""/>
+                        <div class="procuremen-sys-rq-fields">
+                            <input type="date" class="form-control procuremen-sys-rq-date" title="招标截止日期"/>
+                            <div class="procuremen-sys-rq-time-group">
+                                <select class="form-control procuremen-sys-rq-hour" title="时"></select>
+                                <span class="procuremen-sys-rq-colon">:</span>
+                                <select class="form-control procuremen-sys-rq-minute" title="分"></select>
+                            </div>
                         </div>
                     </div>
                 </div>
+                <div class="review-deadline-item">
+                    <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>
             </div>
         </div>
         <div class="review-selected-summary" id="review-selected-summary" aria-live="polite">

+ 21 - 0
application/common/library/ProcuremenTime.php

@@ -180,6 +180,27 @@ class ProcuremenTime
             }
             return '开标验证:' . $content;
         }
+        if ($action === 'audit_append_supplier') {
+            if (preg_match('/^补加供应商/u', $content)) {
+                return $content;
+            }
+
+            return '补加供应商:' . $content;
+        }
+        if ($action === 'audit_resend_sms') {
+            if (preg_match('/重新发送短信/u', $content)) {
+                return $content;
+            }
+
+            return '确认页重新发送短信:' . $content;
+        }
+        if ($action === 'audit_resend_email') {
+            if (preg_match('/重新发送邮件/u', $content)) {
+                return $content;
+            }
+
+            return '确认页重新发送邮件:' . $content;
+        }
         if ($action === 'issue_submit') {
             if (preg_match('/通知供应商[((](\d+)\s*家[))].*?[::]\s*(.+)$/u', $content, $m)) {
                 return '下发:通知供应商(' . $m[1] . ' 家):' . trim($m[2]);

+ 3 - 0
application/extra/purchase_order_delivery_deadline_install.sql

@@ -0,0 +1,3 @@
+-- 采购订单交货截止日期(执行一次即可)
+ALTER TABLE `purchase_order`
+  ADD COLUMN `delivery_deadline` date DEFAULT NULL COMMENT '交货截止日期' AFTER `sys_rq`;

+ 3 - 0
application/extra/purchase_order_detail_lead_days_install.sql

@@ -0,0 +1,3 @@
+-- 供应商报价预估工期(天),执行一次即可
+ALTER TABLE `purchase_order_detail`
+  ADD COLUMN `lead_days` int(10) unsigned DEFAULT NULL COMMENT '预估工期(天)' AFTER `delivery`;

+ 1 - 1
application/extra/site.php

@@ -5,7 +5,7 @@ return array (
   'brand_logo' => 'https://a-7in6-com.oss-cn-hangzhou.aliyuncs.com/xinhua/img/logo1.png',
   'beian' => '',
   'cdnurl' => '',
-  'version' => '1.10.12',
+  'version' => '1.10.24',
   'timezone' => 'Asia/Shanghai',
   'forbiddenip' => '',
   'languages' => 

+ 409 - 51
application/index/controller/Index.php

@@ -1307,6 +1307,85 @@ class Index extends Frontend
         if ($sysRq !== '' && !preg_match('/^0000-00-00/i', $sysRq)) {
             $row['sys_rq'] = $sysRq;
         }
+        $deliveryDeadline = '';
+        foreach (['delivery_deadline', 'Delivery_deadline'] as $dk) {
+            if (array_key_exists($dk, $pl) && $pl[$dk] !== null && $pl[$dk] !== '') {
+                $deliveryDeadline = trim((string)$pl[$dk]);
+                break;
+            }
+            if (array_key_exists($dk, $poRow) && $poRow[$dk] !== null && $poRow[$dk] !== '') {
+                $deliveryDeadline = trim((string)$poRow[$dk]);
+                break;
+            }
+        }
+        if ($deliveryDeadline !== '' && !preg_match('/^0000-00-00/i', $deliveryDeadline)) {
+            $row['delivery_deadline'] = $deliveryDeadline;
+        }
+    }
+
+    /**
+     * 统一取年月日(无效则空串)
+     */
+    protected function mprocFormatYmdValue($raw): string
+    {
+        $s = trim((string)$raw);
+        if ($s === '' || preg_match('/^0000-00-00/i', $s)) {
+            return '';
+        }
+        $s = str_replace(['/', '.'], '-', $s);
+        if (preg_match('/^(\d{4}-\d{2}-\d{2})/', $s, $m)) {
+            return $m[1];
+        }
+        $ts = strtotime(str_replace('T', ' ', $s));
+        if ($ts === false || $ts <= 0) {
+            return '';
+        }
+
+        return date('Y-m-d', $ts);
+    }
+
+    /**
+     * 列表展示:招标截止 / 交货截止
+     *
+     * @param array<string, mixed> $row
+     */
+    protected function mprocEnrichOrderDeadlineDisplay(array &$row): void
+    {
+        $sysRqRaw = $this->mprocResolveSysRqFromPo($row);
+        $bid = $this->mprocFormatYmdValue($sysRqRaw !== '' ? $sysRqRaw : ($row['sys_rq'] ?? ''));
+        $del = $this->mprocFormatYmdValue($row['delivery_deadline'] ?? '');
+        $row['mproc_bid_deadline_display'] = $bid;
+        // 完整截止时间供前端保存前校验(含时分秒)
+        $row['mproc_bid_deadline'] = $sysRqRaw;
+        $row['mproc_delivery_deadline_display'] = $del;
+        $row['mproc_delivery_deadline'] = $del;
+    }
+
+    /**
+     * 明细对应主表的交货截止日期 Y-m-d
+     *
+     * @param array<string, mixed> $row
+     */
+    protected function mprocResolveDeliveryDeadlineYmdForDetailRow(array $row): string
+    {
+        $fromRow = $this->mprocFormatYmdValue($row['delivery_deadline'] ?? $row['mproc_delivery_deadline'] ?? '');
+        if ($fromRow !== '') {
+            return $fromRow;
+        }
+        $sid = (int)($row['scydgy_id'] ?? $row['SCYDGY_ID'] ?? 0);
+        if (!$this->mprocIsValidScydgyRowId($sid)) {
+            return '';
+        }
+        try {
+            $po = Db::table('purchase_order')->where('scydgy_id', $sid)->find();
+        } catch (\Throwable $e) {
+            $po = null;
+        }
+        if (!is_array($po)) {
+            return '';
+        }
+
+        return $this->mprocFormatYmdValue($po['delivery_deadline'] ?? $po['Delivery_deadline'] ?? '');
     }
 
     /**
@@ -1320,12 +1399,17 @@ class Index extends Frontend
             return '';
         }
         $sr = trim((string)($po['sys_rq'] ?? $po['SYS_RQ'] ?? ''));
+        if ($sr === '') {
+            $pl = array_change_key_case($po, CASE_LOWER);
+            $sr = trim((string)($pl['sys_rq'] ?? ''));
+        }
 
         return ($sr !== '' && !preg_match('/^0000-00-00/i', $sr)) ? $sr : '';
     }
 
     /**
      * 手机端:仅当主表设置了 sys_rq 且已到期时视为截止(无截止时间仍可填报)
+     * 保存时务必再调一次,防止页面长时间打开、截止后仍提交
      *
      * @param array<string, mixed>|null $po
      */
@@ -1343,6 +1427,33 @@ class Index extends Frontend
         return time() >= $ts;
     }
 
+    /**
+     * 保存前强制按库中最新招标截止时间校验(避免弹窗久开后仍提交)
+     *
+     * @param array<string, mixed>|null $po
+     */
+    protected function mprocAssertQuoteNotDeadlineReached(?array $po, string $label = ''): void
+    {
+        // 以库中最新主表为准,避免内存中旧快照
+        $freshPo = null;
+        if (is_array($po)) {
+            $sid = (int)($po['scydgy_id'] ?? $po['SCYDGY_ID'] ?? 0);
+            if ($this->mprocIsValidScydgyRowId($sid)) {
+                try {
+                    $freshPo = Db::table('purchase_order')->where('scydgy_id', $sid)->find();
+                } catch (\Throwable $e) {
+                    $freshPo = null;
+                }
+            }
+        }
+        $checkPo = is_array($freshPo) ? $freshPo : $po;
+        if ($this->mprocIsQuoteDeadlineReachedForPo(is_array($checkPo) ? $checkPo : null)) {
+            throw new \InvalidArgumentException(
+                ($label !== '' ? $label : '') . '已超过招标截止时间,不可再提交'
+            );
+        }
+    }
+
     /**
      * 主单已完结后:中标 / 未中标
      *
@@ -1399,13 +1510,31 @@ class Index extends Frontend
             $key = ($ccydh !== '' ? $ccydh : ('eid_' . (int)($row['eid'] ?? 0))) . '|' . $cname;
             if (!isset($groups[$key])) {
                 $groups[$key] = [
-                    'group_key'    => $key,
-                    'CCYDH'        => $ccydh,
-                    'CYJMC'        => trim((string)($row['CYJMC'] ?? '')),
-                    'company_name' => $cname,
-                    'lines'        => [],
+                    'group_key'                        => $key,
+                    'CCYDH'                            => $ccydh,
+                    'CYJMC'                            => trim((string)($row['CYJMC'] ?? '')),
+                    'company_name'                     => $cname,
+                    'mproc_bid_deadline_display'       => trim((string)($row['mproc_bid_deadline_display'] ?? '')),
+                    'mproc_bid_deadline'               => trim((string)($row['mproc_bid_deadline'] ?? '')),
+                    'mproc_delivery_deadline_display'  => trim((string)($row['mproc_delivery_deadline_display'] ?? '')),
+                    'mproc_delivery_deadline'          => trim((string)($row['mproc_delivery_deadline'] ?? '')),
+                    'lines'                            => [],
                 ];
                 $order[] = $key;
+            } else {
+                if ($groups[$key]['mproc_delivery_deadline'] === ''
+                    && trim((string)($row['mproc_delivery_deadline'] ?? '')) !== '') {
+                    $groups[$key]['mproc_delivery_deadline'] = trim((string)$row['mproc_delivery_deadline']);
+                    $groups[$key]['mproc_delivery_deadline_display'] = trim((string)($row['mproc_delivery_deadline_display'] ?? ''));
+                }
+                if ($groups[$key]['mproc_bid_deadline'] === ''
+                    && trim((string)($row['mproc_bid_deadline'] ?? '')) !== '') {
+                    $groups[$key]['mproc_bid_deadline'] = trim((string)$row['mproc_bid_deadline']);
+                }
+                if ($groups[$key]['mproc_bid_deadline_display'] === ''
+                    && trim((string)($row['mproc_bid_deadline_display'] ?? '')) !== '') {
+                    $groups[$key]['mproc_bid_deadline_display'] = trim((string)$row['mproc_bid_deadline_display']);
+                }
             }
             $groups[$key]['lines'][] = $row;
         }
@@ -1477,8 +1606,8 @@ class Index extends Frontend
 
     /**
      * 查询 purchase_order_detail 列表(订单页)
-     * 无搜索词时:左侧 Tab 按 status_name 筛选(未提交/已提交/已完成
-     * 有搜索词时:不按 Tab 筛选,在本单位可见数据内全局关键字匹配
+     * 无搜索词 / 有搜索词:均按左侧 Tab 筛选(draft/submitted/done
+     * 有搜索词时:在当前 Tab 内做关键字匹配
      *
      * @param string      $tab             draft|submitted|done
      * @param string|null $statusNameCol   status_name 真实列名;为 null 时不按 Tab 过滤
@@ -1539,6 +1668,12 @@ class Index extends Frontend
             if (!is_array($row)) {
                 continue;
             }
+            // 废弃明细不在手机端展示
+            if (ProcuremenStatus::isPodVoid($row['status'] ?? '')
+                || trim((string)($row['status_name'] ?? '')) === ProcuremenStatus::POD_VOID) {
+                $row = null;
+                continue;
+            }
             $row['eid'] = (int)($row['id'] ?? $row['ID'] ?? 0);
             $sid = (int)($row['scydgy_id'] ?? $row['SCYDGY_ID'] ?? 0);
             if ($this->mprocIsValidScydgyRowId($sid) && isset($poBySid[$sid])) {
@@ -1586,17 +1721,23 @@ 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);
             $row['mproc_fill_hint'] = '';
             $row['mproc_this_quantity_display'] = $this->mprocResolveDisplayThisQuantity($row);
             $row['mproc_remark'] = $this->mprocResolveDetailRemark($row);
         }
         unset($row);
+        $rows = array_values(array_filter($rows, function ($r) {
+            return is_array($r);
+        }));
+        // 同一供应商同一工序若有多条有效明细(重复下发),只保留最新一条
+        $rows = $this->mprocDedupeDetailRowsByCompanyProcess($rows);
 
-        if (trim((string)$q) === '') {
-            $rows = array_values(array_filter($rows, function ($r) use ($tab) {
-                return is_array($r) && trim((string)($r['mproc_list_tab'] ?? '')) === $tab;
-            }));
-        }
+        // 始终按 Tab 过滤(含搜索):未中标/已截止等只出现在「已完成」
+        $rows = array_values(array_filter($rows, function ($r) use ($tab) {
+            return is_array($r) && trim((string)($r['mproc_list_tab'] ?? '')) === $tab;
+        }));
 
         $groups = $this->mprocGroupRowsByOrder($rows);
 
@@ -1607,6 +1748,48 @@ class Index extends Frontend
         ];
     }
 
+    /**
+     * 同一供应商 + 同一工序只保留一条明细(优先 id 更大的最新记录)
+     *
+     * @param array<int, array<string, mixed>> $rows
+     * @return array<int, array<string, mixed>>
+     */
+    protected function mprocDedupeDetailRowsByCompanyProcess(array $rows): array
+    {
+        $best = [];
+        $orderKeys = [];
+        foreach ($rows as $row) {
+            if (!is_array($row)) {
+                continue;
+            }
+            $cn = trim((string)($row['company_name'] ?? ''));
+            $sid = (int)($row['scydgy_id'] ?? $row['SCYDGY_ID'] ?? 0);
+            $eid = (int)($row['eid'] ?? $row['id'] ?? $row['ID'] ?? 0);
+            if ($this->mprocIsValidScydgyRowId($sid)) {
+                $key = $cn . '|sid:' . $sid;
+            } else {
+                $key = $cn . '|eid:' . $eid;
+            }
+            if (!isset($best[$key])) {
+                $best[$key] = $row;
+                $orderKeys[] = $key;
+                continue;
+            }
+            $prevEid = (int)($best[$key]['eid'] ?? $best[$key]['id'] ?? $best[$key]['ID'] ?? 0);
+            if ($eid > $prevEid) {
+                $best[$key] = $row;
+            }
+        }
+        $out = [];
+        foreach ($orderKeys as $key) {
+            if (isset($best[$key])) {
+                $out[] = $best[$key];
+            }
+        }
+
+        return $out;
+    }
+
     /**
      * status_name → 手机端左侧 Tab
      */
@@ -1733,14 +1916,12 @@ class Index extends Frontend
             }
         }
         $effectiveSn = $this->mprocResolveEffectiveStatusName($row, is_array($poRow) ? $poRow : null);
-        if (trim((string)$q) === '') {
-            $rowTab = $this->mprocResolveListTabForRow($row, is_array($poRow) ? $poRow : null, $effectiveSn);
-            if ($rowTab !== '' && $rowTab !== $tab) {
-                return $bundle;
-            }
+        $rowTab = $this->mprocResolveListTabForRow($row, is_array($poRow) ? $poRow : null, $effectiveSn);
+        if ($rowTab !== '' && $rowTab !== $tab) {
+            return $bundle;
         }
         $row['status_name'] = $effectiveSn;
-        $listTab = $this->mprocResolveListTabForRow($row, is_array($poRow) ? $poRow : null, $effectiveSn);
+        $listTab = $rowTab;
         $row['mproc_list_tab'] = $listTab;
         $row['mproc_deadline_reached'] = $this->mprocIsQuoteDeadlineReachedForPo($poRow) ? 1 : 0;
         $row['mproc_pick_result'] = $this->mprocResolvePickResultText($row, $poRow);
@@ -1764,6 +1945,8 @@ 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);
         $row['mproc_fill_hint'] = '';
         $row['mproc_this_quantity_display'] = $this->mprocResolveDisplayThisQuantity($row);
         array_unshift($rows, $row);
@@ -1966,7 +2149,9 @@ class Index extends Frontend
         }
         $user = $this->mprocSyncSessionCustomerUser($user);
 
-        $tabParam = trim((string)$this->request->get('tab', 'draft'));
+        $tabParamRaw = $this->request->get('tab', null);
+        $hasExplicitTab = ($tabParamRaw !== null && trim((string)$tabParamRaw) !== '');
+        $tabParam = trim((string)($tabParamRaw ?? 'draft'));
         $mainTab = trim((string)$this->request->get('main_tab', 'orders'));
         // 旧地址 ?tab=me 表示「我的」
         if ($tabParam === 'me') {
@@ -1984,17 +2169,23 @@ class Index extends Frontend
         $mprocFocusEid = 0;
         $focusEid = $this->mprocReadFocusEidFromRequest();
         if ($focusEid > 0 && $mainTab === 'orders') {
-            $mprocFocusEid = $focusEid;
-            // 仅邮件/短信直链(URL 带 focus_eid)时自动切 Tab;Session 记忆不覆盖用户手动点的状态
-            $focusFromUrl = (int)$this->request->param('focus_eid', 0) > 0;
-            if (!$focusFromUrl) {
-                $uri = isset($_SERVER['REQUEST_URI']) ? (string)$_SERVER['REQUEST_URI'] : '';
-                $focusFromUrl = $this->mprocParseFocusEidFromUriString($uri) > 0;
-            }
-            if ($focusFromUrl && trim((string)$q) === '') {
-                $resolvedTab = $this->mprocResolveListTabForFocusEid($focusEid, $user);
-                if ($resolvedTab !== '') {
-                    $tab = $resolvedTab;
+            $resolvedTab = $this->mprocResolveListTabForFocusEid($focusEid, $user);
+            // 用户已手动点了其他左侧 Tab:取消定位锁定,避免刷新又跳回
+            if ($hasExplicitTab && $resolvedTab !== '' && $resolvedTab !== $tab) {
+                Session::delete('mproc_focus_eid');
+                $focusEid = 0;
+            } else {
+                $mprocFocusEid = $focusEid;
+                // 仅邮件/短信直链且 URL 未带 tab 时,自动切到定位单所在 Tab
+                $focusFromUrl = (int)$this->request->param('focus_eid', 0) > 0;
+                if (!$focusFromUrl) {
+                    $uri = isset($_SERVER['REQUEST_URI']) ? (string)$_SERVER['REQUEST_URI'] : '';
+                    $focusFromUrl = $this->mprocParseFocusEidFromUriString($uri) > 0;
+                }
+                if ($focusFromUrl && trim((string)$q) === '' && !$hasExplicitTab) {
+                    if ($resolvedTab !== '') {
+                        $tab = $resolvedTab;
+                    }
                 }
             }
         }
@@ -2069,8 +2260,20 @@ class Index extends Frontend
         }
 
         $statusNameCol = $this->mprocResolveProcuremenColumn(['status_name', 'status_txt', 'status_text']);
-        $focusEid = $this->mprocReadFocusEidFromRequest();
-        $focusTab = $focusEid > 0 ? $this->mprocResolveListTabForFocusEid($focusEid, $user) : '';
+        if ((int)$this->request->request('clear_focus', 0) === 1) {
+            Session::delete('mproc_focus_eid');
+            $focusEid = 0;
+            $focusTab = '';
+        } else {
+            $focusEid = $this->mprocReadFocusEidFromRequest();
+            $focusTab = $focusEid > 0 ? $this->mprocResolveListTabForFocusEid($focusEid, $user) : '';
+            // 列表请求已指定 Tab,且与定位单所在 Tab 不同:视为用户离开锁定页
+            if ($focusEid > 0 && $focusTab !== '' && $focusTab !== $tab) {
+                Session::delete('mproc_focus_eid');
+                $focusEid = 0;
+                $focusTab = '';
+            }
+        }
         $bundle = $this->mprocFetchProcuremenList($user, $tab, $q, $statusNameCol);
         if ($focusEid > 0) {
             $bundle = $this->mprocEnsureFocusRowInList($bundle, $focusEid, $user, $statusNameCol, $tab, $q);
@@ -2383,15 +2586,100 @@ class Index extends Frontend
     }
 
     /**
-     * 保存单条协助明细的金额、交期(内部)
+     * 确保 purchase_order_detail.lead_days 字段存在
+     */
+    protected function mprocEnsureLeadDaysColumn(): void
+    {
+        static $done = false;
+        if ($done) {
+            return;
+        }
+        $done = true;
+        try {
+            $cols = Db::query("SHOW COLUMNS FROM `purchase_order_detail` LIKE 'lead_days'");
+            if (empty($cols)) {
+                Db::execute(
+                    "ALTER TABLE `purchase_order_detail` ADD COLUMN `lead_days` int(10) unsigned DEFAULT NULL COMMENT '预估工期(天)' AFTER `delivery`"
+                );
+            }
+        } catch (\Throwable $e) {
+            // 忽略:保存时再按列是否存在处理
+        }
+    }
+
+    /**
+     * 交货日期相对今天的天数(可负数,调用方自行 clamp)
+     */
+    protected function mprocCalcLeadDaysFromDeliveryYmd(string $ymd): ?int
+    {
+        if (!preg_match('/^(\d{4}-\d{2}-\d{2})/', $ymd, $m)) {
+            return null;
+        }
+        $t1 = strtotime(date('Y-m-d') . ' 00:00:00');
+        $t2 = strtotime($m[1] . ' 00:00:00');
+        if ($t1 === false || $t2 === false) {
+            return null;
+        }
+
+        return (int)round(($t2 - $t1) / 86400);
+    }
+
+    protected function mprocAddDaysToToday(int $days): string
+    {
+        $base = strtotime(date('Y-m-d') . ' 00:00:00');
+        if ($base === false) {
+            $base = time();
+        }
+
+        return date('Y-m-d', strtotime('+' . max(0, $days) . ' days', $base));
+    }
+
+    /**
+     * @param array<string, mixed> $row
+     */
+    protected function mprocEnrichLeadDaysDisplay(array &$row): void
+    {
+        $days = null;
+        $raw = $row['lead_days'] ?? null;
+        if ($raw !== null && $raw !== '' && is_numeric($raw)) {
+            $days = max(0, (int)$raw);
+        } else {
+            $dd = trim((string)($row['delivery_display'] ?? ''));
+            if ($dd === '') {
+                $dv = isset($row['delivery']) ? trim((string)$row['delivery']) : '';
+                if ($dv !== '' && preg_match('/^(\d{4}-\d{2}-\d{2})/', $dv, $m)) {
+                    $dd = $m[1];
+                }
+            }
+            if ($dd !== '') {
+                $calc = $this->mprocCalcLeadDaysFromDeliveryYmd($dd);
+                if ($calc !== null) {
+                    $days = max(0, $calc);
+                }
+            }
+        }
+        if ($days !== null) {
+            $row['lead_days'] = $days;
+            $row['lead_days_display'] = (string)$days;
+            $row['lead_days_missing'] = 0;
+        } else {
+            $row['lead_days'] = '';
+            $row['lead_days_display'] = '';
+            $row['lead_days_missing'] = 1;
+        }
+    }
+
+    /**
+     * 保存单条协助明细的金额、交期、工期(内部)
      *
      * @param array<string, mixed> $user
      * @param int                  $id
      * @param string               $amountRaw
      * @param string               $deliveryRaw
+     * @param string|int|null      $leadDaysRaw
      * @return string 工序名(用于批量错误提示)
      */
-    protected function mprocSaveDetailQuote(array $user, int $id, string $amountRaw, string $deliveryRaw): string
+    protected function mprocSaveDetailQuote(array $user, int $id, string $amountRaw, string $deliveryRaw, $leadDaysRaw = null): string
     {
         if ($id <= 0) {
             throw new \InvalidArgumentException('参数错误');
@@ -2429,25 +2717,58 @@ class Index extends Frontend
         if (in_array($effectiveSn, ['已完成', '未通过', '已废弃'], true)) {
             throw new \InvalidArgumentException($label . '已结束,不能再修改');
         }
-        if ($this->mprocIsQuoteDeadlineReachedForPo(is_array($po) ? $po : null)) {
-            throw new \InvalidArgumentException($label . '报价已截止,不能再修改');
-        }
+        $this->mprocAssertQuoteNotDeadlineReached(is_array($po) ? $po : null, $label);
         if (!$this->mprocCanEditRow($user, array_merge($row, ['status_name' => $effectiveSn]), is_array($po) ? $po : null)) {
             if (!empty($user['is_admin'])) {
                 throw new \InvalidArgumentException('当前账号仅可查看,不能修改单价与交货日期');
             }
+            // 截止后 canEdit=false,统一给明确文案
+            if ($this->mprocIsQuoteDeadlineReachedForPo(is_array($po) ? $po : null)) {
+                throw new \InvalidArgumentException($label . '已超过招标截止时间,不可再提交');
+            }
             throw new \InvalidArgumentException($label . '无权修改');
         }
 
+        $this->mprocEnsureLeadDaysColumn();
+
         $amountRaw = trim($amountRaw);
         $deliveryRaw = trim($deliveryRaw);
+        $leadRaw = trim((string)($leadDaysRaw ?? ''));
+        $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) {
+            if (!$hasAmt) {
+                throw new \InvalidArgumentException($label . '请填写单价');
+            }
+            if (!$hasLead) {
+                throw new \InvalidArgumentException($label . '请填写工期');
+            }
+            throw new \InvalidArgumentException($label . '请填写交期');
+        }
+
+        $leadDays = null;
+        if ($leadRaw !== '') {
+            if (!preg_match('/^\d+$/', $leadRaw)) {
+                throw new \InvalidArgumentException($label . '工期须为非负整数(天)');
+            }
+            $leadDays = (int)$leadRaw;
+        }
+
         $data = [];
         if ($amountRaw === '') {
             $data['amount'] = null;
         } else {
-            if (!preg_match('/^-?\d+(\.\d{1,5})?$/', $amountRaw)) {
+            if (!preg_match('/^\d+(\.\d{1,5})?$/', $amountRaw)) {
                 throw new \InvalidArgumentException($label . '单价格式不正确,最多五位小数');
             }
+            if ((float)$amountRaw <= 0) {
+                throw new \InvalidArgumentException($label . '单价必须大于0');
+            }
             $ceilingLimit = $this->mprocResolveCeilingPriceForDetailRow($row);
             if ($ceilingLimit !== null && (float)$amountRaw > $ceilingLimit) {
                 throw new \InvalidArgumentException(
@@ -2479,6 +2800,32 @@ class Index extends Frontend
             }
             $data['delivery'] = date('Y-m-d H:i:s', $ts);
         }
+
+        // 工期 ↔ 交货日期互通:仅填工期则推交期;仅填交期则推工期
+        if (($data['delivery'] === null || $data['delivery'] === '') && $leadDays !== null) {
+            $data['delivery'] = $this->mprocAddDaysToToday($leadDays) . ' ' . date('H:i:s');
+        }
+        if ($leadDays === null && !empty($data['delivery'])) {
+            $ymd = substr((string)$data['delivery'], 0, 10);
+            $calc = $this->mprocCalcLeadDaysFromDeliveryYmd($ymd);
+            if ($calc !== null) {
+                $leadDays = max(0, $calc);
+            }
+        }
+        $data['lead_days'] = $leadDays;
+
+        if (!empty($data['delivery'])) {
+            $deliveryYmd = substr((string)$data['delivery'], 0, 10);
+            if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $deliveryYmd) && $deliveryYmd < date('Y-m-d')) {
+                throw new \InvalidArgumentException($label . '交货日期不能早于今天');
+            }
+            $orderDeliveryDeadline = $this->mprocResolveDeliveryDeadlineYmdForDetailRow($row);
+            if ($orderDeliveryDeadline !== '' && preg_match('/^\d{4}-\d{2}-\d{2}$/', $deliveryYmd)
+                && $deliveryYmd > $orderDeliveryDeadline) {
+                throw new \InvalidArgumentException($label . '交货日期不能超过交货截止日期 ' . $orderDeliveryDeadline);
+            }
+        }
+
         $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');
@@ -2513,7 +2860,7 @@ class Index extends Frontend
         } catch (\Throwable $e) {
             $msg = $e->getMessage();
             if (stripos($msg, 'Unknown column') !== false) {
-                $msg = '请确认数据表 purchase_order_detail 已包含 amount、delivery 字段';
+                $msg = '请确认数据表 purchase_order_detail 已包含 amount、delivery、lead_days 字段';
             }
             throw new \RuntimeException('保存失败:' . $msg);
         }
@@ -2570,13 +2917,14 @@ class Index extends Frontend
         if (in_array($effectiveSn, ['已完成', '未通过', '已废弃'], true)) {
             throw new \InvalidArgumentException('订单已结束,不能再修改备注');
         }
-        if ($this->mprocIsQuoteDeadlineReachedForPo(is_array($po) ? $po : null)) {
-            throw new \InvalidArgumentException('报价已截止,不能再修改备注');
-        }
+        $this->mprocAssertQuoteNotDeadlineReached(is_array($po) ? $po : null);
         if (!$this->mprocCanEditRow($user, array_merge($row, ['status_name' => $effectiveSn]), is_array($po) ? $po : null)) {
             if (!empty($user['is_admin'])) {
                 throw new \InvalidArgumentException('当前账号仅可查看,不能修改备注');
             }
+            if ($this->mprocIsQuoteDeadlineReachedForPo(is_array($po) ? $po : null)) {
+                throw new \InvalidArgumentException('已超过招标截止时间,不可再提交');
+            }
             throw new \InvalidArgumentException('无权修改备注');
         }
 
@@ -2627,9 +2975,9 @@ class Index extends Frontend
     }
 
     /**
-     * 保存协助明细金额、交期
-     * 单条:POST id、amount、delivery
-     * 批量:POST items=[{id,amount,delivery},...] JSON
+     * 保存协助明细金额、交期、工期
+     * 单条:POST id、amount、delivery、lead_days
+     * 批量:POST items=[{id,amount,delivery,lead_days},...] JSON
      */
     public function mprocSave()
     {
@@ -2668,9 +3016,10 @@ class Index extends Frontend
                 $this->error('保存失败,请关闭弹窗后重试');
             }
             $items = [[
-                'id'       => $id,
-                'amount'   => (string)$this->request->post('amount', '', null),
-                'delivery' => (string)$this->request->post('delivery', '', null),
+                'id'        => $id,
+                'amount'    => (string)$this->request->post('amount', '', null),
+                'delivery'  => (string)$this->request->post('delivery', '', null),
+                'lead_days' => (string)$this->request->post('lead_days', '', null),
             ]];
         }
 
@@ -2696,17 +3045,26 @@ class Index extends Frontend
                 if (preg_match('/^(\d{4})[\/.\-](\d{1,2})[\/.\-](\d{1,2})$/', trim($delivery), $dm)) {
                     $delivery = sprintf('%04d-%02d-%02d', (int)$dm[1], (int)$dm[2], (int)$dm[3]);
                 }
+                $leadDays = (string)($it['lead_days'] ?? $it['leadDays'] ?? '');
+                $amount = trim((string)($it['amount'] ?? ''));
+                $leadDays = trim($leadDays);
+                $delivery = trim($delivery);
+                // 全空工序跳过:多工序时允许只保存已填全的
+                if ($amount === '' && $leadDays === '' && $delivery === '') {
+                    continue;
+                }
                 $this->mprocSaveDetailQuote(
                     $user,
                     $id,
-                    (string)($it['amount'] ?? ''),
-                    $delivery
+                    $amount,
+                    $delivery,
+                    $leadDays
                 );
                 $saved++;
                 $savedIds[] = $id;
             }
             if ($saved < 1) {
-                throw new \InvalidArgumentException('没有可保存的工序,请刷新后重试');
+                throw new \InvalidArgumentException('请填写单价、工期、交期');
             }
             $this->mprocSaveOrderGroupRemark($user, $savedIds, $remarkRaw);
             Db::commit();

Plik diff jest za duży
+ 663 - 78
application/index/view/index/index.html


+ 4 - 0
public/assets/css/backend.css

@@ -1209,6 +1209,10 @@ table.table-nowrap thead > tr > th {
   -moz-box-shadow: 0 0 3px #eee;
   box-shadow: 0 0 3px #eee;
 }
+/* Layer 弹层 z-index 约 19891014+,提示需压在遮罩之上,避免被盖住 */
+#toast-container {
+  z-index: 21000000 !important;
+}
 .layui-layer-fast {
   /*自定义底部灰色操作区*/
 }

+ 5 - 0
public/assets/css/xinhua-theme.css

@@ -697,6 +697,11 @@ body.inside-aside .content,
     box-shadow: 0 0 0 2px rgba(22, 119, 255, 0.1) !important;
 }
 
+/* Toastr 提示压过 Layer 弹层遮罩 */
+#toast-container {
+    z-index: 21000000 !important;
+}
+
 @media (max-width: 767px) {
     .skin-blue-light .main-header .navbar {
         background: #ffffff !important;

+ 1 - 0
public/assets/js/backend/customer.js

@@ -65,6 +65,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                         }},
                         {field: 'email', title: __('Email'), operate: 'LIKE', table: table, class: 'autocontent', formatter: Table.api.formatter.content},
                         {field: 'phone', title: __('Phone'), operate: 'LIKE', table: table, class: 'autocontent', formatter: Table.api.formatter.content},
+                        {field: 'score', title: __('Score'), operate: 'BETWEEN', sortable: true},
                         {field: 'company_type', title: __('Company_type'), operate: 'LIKE', table: table, class: 'autocontent', formatter: Table.api.formatter.content},
                         {
                             field: 'status',

+ 766 - 11
public/assets/js/backend/procuremen.js

@@ -210,11 +210,46 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
         return (n < 10 ? '0' : '') + n;
     }
 
+    function procuremenTodayYmd() {
+        if (typeof moment === 'function') {
+            return moment().format('YYYY-MM-DD');
+        }
+        var d = new Date();
+        return d.getFullYear() + '-' + procuremenPad2(d.getMonth() + 1) + '-' + procuremenPad2(d.getDate());
+    }
+
+    /** 日期控件禁止选择今天之前(min=今天) */
+    function procuremenApplyDateMinToday($input, tipMsg) {
+        if (!$input || !$input.length) {
+            return;
+        }
+        var today = procuremenTodayYmd();
+        $input.attr('min', today);
+        var v = ($input.val() || '').trim();
+        if (v && v < today) {
+            $input.val(today);
+        }
+        $input.off('change.procuremenMinToday input.procuremenMinToday')
+            .on('change.procuremenMinToday input.procuremenMinToday', function () {
+                var t = procuremenTodayYmd();
+                var cur = ($(this).val() || '').trim();
+                if (cur && cur < t) {
+                    $(this).val(t);
+                    if (tipMsg && typeof Toastr !== 'undefined' && typeof Toastr.warning === 'function') {
+                        Toastr.warning(tipMsg);
+                    }
+                }
+            });
+    }
+
     function procuremenDefaultSysRqValue() {
+        // 默认取当前时间+1分钟,避免同一分钟内默认值已过期
         if (typeof moment === 'function') {
-            return moment().format('YYYY-MM-DD HH:mm:ss');
+            return moment().add(1, 'minutes').seconds(0).format('YYYY-MM-DD HH:mm:ss');
         }
         var d = new Date();
+        d.setMinutes(d.getMinutes() + 1);
+        d.setSeconds(0);
         return d.getFullYear() + '-' + procuremenPad2(d.getMonth() + 1) + '-' + procuremenPad2(d.getDate())
             + ' ' + procuremenPad2(d.getHours()) + ':' + procuremenPad2(d.getMinutes()) + ':00';
     }
@@ -232,6 +267,118 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
         return procuremenDefaultSysRqValue();
     }
 
+    /** 初选/补加:进页即把供应商列表锁成「铺满到底栏」的固定高度,避免点复选框后才跳变 */
+    /**
+     * 钉住供应商列表区高度(仅进页/加载时用)。
+     * 勾选/取消勾选切勿再调用:重复按 offset 重算会令表格高度跳动。
+     * 定稿后复用同一高度,避免二次测量漂移。
+     */
+    function procuremenPinSupplierPickerLayout(force) {
+        var $form = $('.review-dialog-form').first();
+        if (!$form.length) {
+            return;
+        }
+        var $panel = $form.find('.review-company-panel').first();
+        var $footer = $form.find('.layer-footer').first();
+        var $right = $form.find('.review-split-right').first();
+        if (!$panel.length || !$right.length) {
+            return;
+        }
+        var applyHeight = function (h) {
+            var px = h + 'px';
+            // 覆盖样式表里的 height:auto !important,否则钉高无效
+            try {
+                $right[0].style.setProperty('height', px, 'important');
+                $right[0].style.setProperty('min-height', px, 'important');
+                $right[0].style.setProperty('max-height', px, 'important');
+                $panel[0].style.setProperty('height', px, 'important');
+                $panel[0].style.setProperty('min-height', px, 'important');
+                $panel[0].style.setProperty('max-height', px, 'important');
+            } catch (eStyle) {
+                $right.css({height: px, minHeight: px, maxHeight: px});
+                $panel.css({height: px, minHeight: px, maxHeight: px});
+            }
+        };
+        var measureAndApply = function () {
+            try {
+                $('html, body').css({
+                    height: '100%',
+                    maxHeight: '100%',
+                    overflow: 'hidden',
+                    margin: 0
+                });
+            } catch (eBody) {
+            }
+            $form.css({
+                height: '100%',
+                maxHeight: '100%',
+                overflow: 'hidden'
+            });
+            // 勿清空 .review-selected-summary 的 height/maxHeight,否则已选区会随勾选撑开并把列表顶矮
+            $form.find('.review-split-left').css({flex: '0 0 auto'});
+            var winH = $(window).height() || 0;
+            if (winH < 200) {
+                return 0;
+            }
+            var footerH = $footer.length ? ($footer.outerHeight(true) || 48) : 48;
+            var top = 0;
+            try {
+                top = Math.floor($panel.offset().top || 0);
+            } catch (eTop) {
+                top = 0;
+            }
+            var h = Math.floor(winH - top - footerH - 2);
+            if (h < 260) {
+                h = 260;
+            }
+            applyHeight(h);
+            return h;
+        };
+        var pinned = parseInt($form.data('procuremenPinnedPanelH'), 10) || 0;
+        if (pinned >= 260 && !force) {
+            applyHeight(pinned);
+            return;
+        }
+        // 弹层动画结束后的强制重测:测一次即定稿,避免连串 setTimeout 反复跳
+        if (force) {
+            var hf = measureAndApply();
+            if (hf >= 260) {
+                $form.data('procuremenPinnedPanelH', hf);
+            }
+            return;
+        }
+        measureAndApply();
+        setTimeout(function () {
+            if ($form.data('procuremenPinnedPanelH')) {
+                return;
+            }
+            measureAndApply();
+        }, 50);
+        setTimeout(function () {
+            if ($form.data('procuremenPinnedPanelH')) {
+                return;
+            }
+            measureAndApply();
+        }, 200);
+        setTimeout(function () {
+            if ($form.data('procuremenPinnedPanelH')) {
+                return;
+            }
+            var h = measureAndApply();
+            if (h >= 260) {
+                $form.data('procuremenPinnedPanelH', h);
+            }
+        }, 450);
+        if (typeof window.requestAnimationFrame === 'function') {
+            window.requestAnimationFrame(function () {
+                if ($form.data('procuremenPinnedPanelH')) {
+                    return;
+                }
+                measureAndApply();
+            });
+        }
+    }
+
     /** 截止时间:日期 + 时下拉 + 分下拉(options.readonly 时仅展示不可改) */
     function procuremenInitSysRqSplit($wrap, eventNs, options) {
         if (!$wrap || !$wrap.length) {
@@ -240,6 +387,9 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
         options = options || {};
         var readonly = !!options.readonly;
         var $hidden = $wrap.find('input[type="hidden"][id$="-sys-rq"]');
+        if (!$hidden.length) {
+            $hidden = $wrap.find('input[type="hidden"]').first();
+        }
         var $date = $wrap.find('.procuremen-sys-rq-date');
         var $hour = $wrap.find('.procuremen-sys-rq-hour');
         var $minute = $wrap.find('.procuremen-sys-rq-minute');
@@ -293,9 +443,123 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
             return;
         }
 
+        procuremenApplyDateMinToday($date, '招标截止日期不能早于今天');
+
+        // 选今天时:过去的时/分直接不渲染(不灰显),避免客户误以为点不了
+        var rebuildHourOptions = function (minH) {
+            var keep = $hour.val();
+            var html = [];
+            var h;
+            for (h = minH; h < 24; h++) {
+                html.push('<option value="' + procuremenPad2(h) + '">' + procuremenPad2(h) + '</option>');
+            }
+            $hour.html(html.join(''));
+            if (keep !== null && keep !== undefined && parseInt(keep, 10) >= minH) {
+                $hour.val(procuremenPad2(parseInt(keep, 10)));
+            } else if (html.length) {
+                $hour.val(procuremenPad2(minH));
+            }
+        };
+        var rebuildMinuteOptions = function (minM) {
+            var keep = $minute.val();
+            var html = [];
+            var m;
+            for (m = minM; m < 60; m++) {
+                html.push('<option value="' + procuremenPad2(m) + '">' + procuremenPad2(m) + '</option>');
+            }
+            $minute.html(html.join(''));
+            if (keep !== null && keep !== undefined && parseInt(keep, 10) >= minM) {
+                $minute.val(procuremenPad2(parseInt(keep, 10)));
+            } else if (html.length) {
+                $minute.val(procuremenPad2(minM));
+            }
+        };
+
+        var refreshTimeOptionsForToday = function (showTip) {
+            var today = procuremenTodayYmd();
+            var d = ($date.val() || '').trim();
+            var now = new Date();
+            var curH = now.getHours();
+            var curM = now.getMinutes();
+            var isToday = (d === today);
+            var prevH = parseInt($hour.val(), 10);
+            var prevM = parseInt($minute.val(), 10);
+            var changed = false;
+
+            if (!isToday) {
+                rebuildHourOptions(0);
+                rebuildMinuteOptions(0);
+                if (!isNaN(prevH)) {
+                    $hour.val(procuremenPad2(prevH));
+                }
+                if (!isNaN(prevM)) {
+                    $minute.val(procuremenPad2(prevM));
+                }
+                syncToHidden();
+                return;
+            }
+
+            // 当天:只列出当前时刻之后的时分
+            var minH = curH;
+            var minM = curM + 1;
+            if (minM >= 60) {
+                minM = 0;
+                minH = curH + 1;
+            }
+            if (minH > 23) {
+                // 当天已无可用时间,跳到明天 00:00
+                var t = new Date();
+                t.setDate(t.getDate() + 1);
+                var nd = t.getFullYear() + '-' + procuremenPad2(t.getMonth() + 1) + '-' + procuremenPad2(t.getDate());
+                $date.val(nd);
+                rebuildHourOptions(0);
+                rebuildMinuteOptions(0);
+                $hour.val('00');
+                $minute.val('00');
+                changed = true;
+            } else {
+                rebuildHourOptions(minH);
+                var selH = parseInt($hour.val(), 10);
+                if (isNaN(selH) || selH < minH) {
+                    selH = minH;
+                    $hour.val(procuremenPad2(selH));
+                    changed = true;
+                }
+                if (selH === minH) {
+                    rebuildMinuteOptions(minM);
+                    var selM = parseInt($minute.val(), 10);
+                    if (isNaN(selM) || selM < minM) {
+                        $minute.val(procuremenPad2(minM));
+                        changed = true;
+                    }
+                } else {
+                    rebuildMinuteOptions(0);
+                    if (!isNaN(prevM)) {
+                        $minute.val(procuremenPad2(prevM));
+                    }
+                }
+                if (!isNaN(prevH) && prevH < minH) {
+                    changed = true;
+                }
+                if (!isNaN(prevM) && selH === minH && prevM < minM) {
+                    changed = true;
+                }
+            }
+
+            syncToHidden();
+            if (changed && showTip && typeof Toastr !== 'undefined' && typeof Toastr.warning === 'function') {
+                Toastr.warning('当天截止时间不能早于当前时间');
+            }
+        };
+
+        refreshTimeOptionsForToday(false);
+
         $date.add($hour).add($minute)
             .off('change.' + eventNs + ' input.' + eventNs)
-            .on('change.' + eventNs + ' input.' + eventNs, syncToHidden);
+            .on('change.' + eventNs + ' input.' + eventNs, function () {
+                refreshTimeOptionsForToday(true);
+                syncToHidden();
+            });
     }
 
     function procuremenFocusSysRqSplit($wrap) {
@@ -1412,7 +1676,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                     sessionStorage.setItem('procuremen_merge_rows', JSON.stringify(selRows));
                 } catch (ignoreStore) {
                 }
-                var titlePick = ' ';
+                var titlePick = '初选供应商';
                 var revUrl = Fast.api.fixurl('procuremen/pickreview?ids=' + encodeURIComponent(ids[0]) + '&merge=1');
                 var winPick = window;
                 if (!winPick.Backend || !winPick.Backend.api) {
@@ -1492,7 +1756,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                 );
                 Layer.confirm(deleteMsg, $.extend({}, procuremenConfirmLayerBase, {
                     title: '删除',
-                    btn: ['确删除', '取消']
+                    btn: ['确删除', '取消']
                 }), function (idxDel) {
                     Layer.close(idxDel);
                     Fast.api.ajax({
@@ -1525,7 +1789,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                 );
                 Layer.confirm(finishBatchMsg, $.extend({}, procuremenConfirmLayerBase, {
                     title: '完结确认',
-                    btn: ['确', '取消']
+                    btn: ['确', '取消']
                 }), function (idxFin) {
                     Layer.close(idxFin);
                     var tokenFin = $('input[name=\'__token__\']').val() || '';
@@ -1927,7 +2191,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                         }
                         var winA = window;
                         if (winA.Backend && winA.Backend.api) {
-                            winA.Backend.api.open(auditUrl, '供应商报价明细', {area: ['98%', '92%']});
+                            winA.Backend.api.open(auditUrl, '供应商报价明细', {area: ['100%', '100%']});
                         }
                         return;
                     }
@@ -2477,7 +2741,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                     title: '采购终审 — 审批通过',
                     area: ['480px', 'auto'],
                     offset: 'auto',
-                    btn: ['确提交', '取消'],
+                    btn: ['确提交', '取消'],
                     fixed: true,
                     shade: 0.35,
                     zIndex: procuremenLayerNextZIndex(),
@@ -2611,8 +2875,33 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
         review: function () {
             Controller.api.bindevent();
             procuremenInitSysRqSplit($('#review-sys-rq-wrap'), 'reviewSysRq');
+            procuremenPinSupplierPickerLayout();
+
+            (function () {
+                var $dd = $('#review-delivery-deadline');
+                if (!$dd.length) {
+                    return;
+                }
+                procuremenApplyDateMinToday($dd, '交货截止日期不能早于今天');
+                var syncDeliveryDeadlineUi = function () {
+                    $dd.toggleClass('has-value', !!(($dd.val() || '').trim()));
+                };
+                $dd.on('change input', syncDeliveryDeadlineUi);
+                syncDeliveryDeadlineUi();
+            })();
 
             function procuremenReviewWarn(msg) {
+                // 弹层 iframe 内优先本页提示,避免被父页 Layer 遮罩盖住
+                var inDialog = false;
+                try {
+                    inDialog = !!(window.frameElement) || ($('body').hasClass('is-dialog'));
+                } catch (eIn) {
+                    inDialog = false;
+                }
+                if (inDialog && typeof Toastr !== 'undefined' && typeof Toastr.warning === 'function') {
+                    Toastr.warning(msg);
+                    return;
+                }
                 var shown = false;
                 try {
                     if (typeof parent !== 'undefined' && parent.Toastr && typeof parent.Toastr.warning === 'function') {
@@ -2695,10 +2984,39 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                 }
                 var sysRq = ($rq.val() || '').trim();
                 if (!sysRq) {
-                    procuremenReviewWarn('请选择截止时间');
+                    procuremenReviewWarn('请选择招标截止日期');
                     procuremenFocusSysRqSplit($rqWrap);
                     return;
                 }
+                var todayYmd = procuremenTodayYmd();
+                var bidYmd = (sysRq.match(/^(\d{4}-\d{2}-\d{2})/) || [])[1] || '';
+                if (!bidYmd || bidYmd < todayYmd) {
+                    procuremenReviewWarn('招标截止日期不能早于今天');
+                    procuremenFocusSysRqSplit($rqWrap);
+                    return;
+                }
+                var bidTs = Date.parse(String(sysRq).replace(/-/g, '/'));
+                if (!bidTs || isNaN(bidTs) || bidTs <= Date.now()) {
+                    procuremenReviewWarn('招标截止时间不能早于当前时间');
+                    procuremenFocusSysRqSplit($rqWrap);
+                    return;
+                }
+                var $deliveryDeadline = $('#review-delivery-deadline');
+                var deliveryDeadline = ($deliveryDeadline.val() || '').trim();
+                if (!deliveryDeadline) {
+                    procuremenReviewWarn('请选择交货截止日期');
+                    try {
+                        $deliveryDeadline.focus();
+                    } catch (ignoreFocus) {}
+                    return;
+                }
+                if (deliveryDeadline < todayYmd) {
+                    procuremenReviewWarn('交货截止日期不能早于今天');
+                    try {
+                        $deliveryDeadline.focus();
+                    } catch (ignoreFocus2) {}
+                    return;
+                }
                 var cons;
                 try {
                     cons = JSON.parse($('#c-row-json').val() || '{}');
@@ -2728,7 +3046,8 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                         row_json: JSON.stringify(cons),
                         merge_rows_json: mergePayload,
                         companies_json: JSON.stringify(selected),
-                        sys_rq: sysRq
+                        sys_rq: sysRq,
+                        delivery_deadline: deliveryDeadline
                     }
                 }, function (data, ret) {
                     var msg = (ret && ret.msg) ? ret.msg : '操作成功';
@@ -3374,6 +3693,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                         $masterF.prop('indeterminate', nF > 0 && nF < $cbsF.length);
                     }
                     syncReviewSelectedSummary();
+                    procuremenPinSupplierPickerLayout();
                     return false;
                 });
             }
@@ -3413,6 +3733,403 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
             });
         },
 
+        auditappend: function () {
+            var existingNames = {};
+            try {
+                var rawEx = $('#audit-append-existing-json').val() || '[]';
+                var arrEx = JSON.parse(rawEx);
+                if ($.isArray(arrEx)) {
+                    $.each(arrEx, function (i, n) {
+                        var t = String(n || '').trim();
+                        if (t) {
+                            existingNames[t] = true;
+                        }
+                    });
+                }
+            } catch (eEx) {
+                existingNames = {};
+            }
+
+            var reviewCompaniesAll = [];
+            var activeReviewCategory = null;
+            var reviewCompanySearchQ = '';
+            var reviewSelectedMap = {};
+            var reviewSearchTimer = null;
+
+            function isExistingCompany(c) {
+                var n = String((c && (c.name || c.company_name)) || '').trim();
+                return !!(n && existingNames[n]);
+            }
+
+            function syncReviewSelectedSummary() {
+                var entries = [];
+                $.each(reviewSelectedMap, function (k, c) {
+                    if (!c || isExistingCompany(c)) {
+                        return;
+                    }
+                    var t = String(c.name || c.company_name || '').trim() || '(无名称)';
+                    entries.push({k: k, label: t});
+                });
+                entries.sort(function (a, b) {
+                    return a.label.localeCompare(b.label, 'zh-CN');
+                });
+                var n = entries.length;
+                var $summary = $('#review-selected-summary');
+                $('#review-selected-count').text(String(n));
+                $summary.toggleClass('is-empty', n === 0);
+                var $tags = $('#review-selected-tags').empty();
+                if (!n) {
+                    return;
+                }
+                $.each(entries, function (i, e) {
+                    var $chip = $('<span class="review-selected-chip"/>').attr('title', e.label);
+                    $chip.data('reviewSelKey', e.k);
+                    $chip.append(
+                        $('<span class="review-chip-text"/>').text(e.label),
+                        $('<button type="button" class="review-chip-remove" title="取消选择"/>').text('\u00d7')
+                    );
+                    $tags.append($chip);
+                });
+            }
+
+            function companyMatchesCategory(c, cat) {
+                if (!cat) {
+                    return true;
+                }
+                var rawCat = (c.company_type != null && String(c.company_type).trim() !== '')
+                    ? String(c.company_type).trim()
+                    : String(c.category || '').trim();
+                var segs = [];
+                if (!rawCat) {
+                    segs = ['未分类'];
+                } else {
+                    var seen = {};
+                    $.each(String(rawCat).split('、'), function (idx, p) {
+                        var tt = String(p).trim();
+                        if (!tt || seen[tt]) {
+                            return;
+                        }
+                        seen[tt] = true;
+                        segs.push(tt);
+                    });
+                    if (!segs.length) {
+                        segs = ['未分类'];
+                    }
+                }
+                return $.inArray(cat, segs) !== -1;
+            }
+
+            function filteredCompanyList() {
+                var q = reviewCompanySearchQ;
+                var searchActive = q.length > 0;
+                var list = [];
+                $.each(reviewCompaniesAll, function (i, c) {
+                    if (!c || typeof c !== 'object') {
+                        return;
+                    }
+                    if (!searchActive && activeReviewCategory !== null && !companyMatchesCategory(c, activeReviewCategory)) {
+                        return;
+                    }
+                    if (q) {
+                        var blob = [
+                            c.name, c.company_name, c.username, c.email, c.phone,
+                            c.company_type, c.category
+                        ].map(function (x) {
+                            return x == null ? '' : String(x);
+                        }).join(' ').toLowerCase();
+                        if (blob.indexOf(q) === -1) {
+                            return;
+                        }
+                    }
+                    list.push(c);
+                });
+                return list;
+            }
+
+            function renderCompanyRows() {
+                var list = filteredCompanyList();
+                var $tbody = $('#review-company-tbody').empty();
+                if (!list.length) {
+                    $tbody.append($('<tr/>').append($('<td colspan="6" class="text-muted text-center"/>').text('暂无符合条件的单位')));
+                    $('#review-check-all').prop('checked', false).prop('indeterminate', false);
+                    syncReviewSelectedSummary();
+                    return;
+                }
+                $.each(list, function (i, c) {
+                    var existing = isExistingCompany(c);
+                    var $cb = $('<input type="checkbox" class="review-company-cb"/>');
+                    $cb.data('company', c);
+                    if (existing) {
+                        $cb.prop('disabled', true).prop('checked', true);
+                    } else {
+                        var k = procuremenReviewCompanyKey(c);
+                        if (k && reviewSelectedMap[k]) {
+                            $cb.prop('checked', true);
+                        }
+                    }
+                    var $tr = $('<tr/>');
+                    if (existing) {
+                        $tr.addClass('is-existing').attr('title', '该供应商已在本单中');
+                    }
+                    $tr.append($('<td class="review-td-cb"/>').append($cb));
+                    $tr.append($('<td/>').text((c.name || c.company_name || '') + (existing ? '(已下发)' : '')));
+                    $tr.append($('<td/>').text(c.username || ''));
+                    $tr.append($('<td/>').text(c.email || ''));
+                    $tr.append($('<td/>').text(c.phone || ''));
+                    $tr.append($('<td/>').text((c.company_type != null && String(c.company_type).trim() !== '') ? String(c.company_type).trim() : (c.category || '')));
+                    $tbody.append($tr);
+                });
+                var $cbs = $('#review-company-tbody .review-company-cb:not(:disabled)');
+                var $master = $('#review-check-all');
+                if (!$cbs.length) {
+                    $master.prop('checked', false).prop('indeterminate', false);
+                } else {
+                    var n = $cbs.filter(':checked').length;
+                    $master.prop('checked', n === $cbs.length);
+                    $master.prop('indeterminate', n > 0 && n < $cbs.length);
+                }
+                syncReviewSelectedSummary();
+            }
+
+            function rebuildCategorySidebar() {
+                var counts = {};
+                var total = 0;
+                $.each(reviewCompaniesAll, function (i, c) {
+                    if (!c || typeof c !== 'object') {
+                        return;
+                    }
+                    total++;
+                    var raw = (c.company_type != null && String(c.company_type).trim() !== '')
+                        ? String(c.company_type).trim()
+                        : String(c.category || '').trim();
+                    var segs = [];
+                    if (!raw) {
+                        segs = ['未分类'];
+                    } else {
+                        var seen = {};
+                        $.each(String(raw).split('、'), function (idx, p) {
+                            var tt = String(p).trim();
+                            if (!tt || seen[tt]) {
+                                return;
+                            }
+                            seen[tt] = true;
+                            segs.push(tt);
+                        });
+                        if (!segs.length) {
+                            segs = ['未分类'];
+                        }
+                    }
+                    $.each(segs, function (j, seg) {
+                        counts[seg] = (counts[seg] || 0) + 1;
+                    });
+                });
+                var cats = Object.keys(counts).sort(function (a, b) {
+                    return a.localeCompare(b, 'zh-CN');
+                });
+                var $list = $('#review-category-list').empty();
+                var $all = $('<div class="review-cat-item review-cat-all"/>').append(
+                    $('<span/>').text('全部 '),
+                    $('<span class="review-cat-count"/>').text('(' + total + ')')
+                );
+                $all.toggleClass('active', activeReviewCategory === null);
+                $list.append($all);
+                $.each(cats, function (i, cat) {
+                    var $it = $('<div class="review-cat-item"/>').append(
+                        $('<span/>').text(cat + ' '),
+                        $('<span class="review-cat-count"/>').text('(' + counts[cat] + ')')
+                    );
+                    $it.data('reviewCat', cat);
+                    $it.toggleClass('active', activeReviewCategory === cat);
+                    $list.append($it);
+                });
+            }
+
+            function loadCompanies() {
+                Fast.api.ajax({
+                    url: 'procuremen/reviewCompanies',
+                    type: 'GET',
+                    loading: false
+                }, function (data) {
+                    reviewCompaniesAll = Array.isArray(data) ? data : [];
+                    reviewSelectedMap = {};
+                    activeReviewCategory = null;
+                    reviewCompanySearchQ = '';
+                    $('#review-company-search').val('');
+                    rebuildCategorySidebar();
+                    renderCompanyRows();
+                    return false;
+                });
+            }
+
+            if (typeof procuremenInitSysRqSplit === 'function') {
+                procuremenInitSysRqSplit($('#audit-append-sys-rq-wrap'), 'auditAppend', {readonly: true});
+            }
+
+            $(document).off('click.auditAppendCat', '#review-category-list .review-cat-item')
+                .on('click.auditAppendCat', '#review-category-list .review-cat-item', function () {
+                    if ($(this).hasClass('review-cat-all')) {
+                        activeReviewCategory = null;
+                    } else {
+                        activeReviewCategory = $(this).data('reviewCat') || null;
+                    }
+                    $('#review-category-list .review-cat-item').removeClass('active');
+                    $(this).addClass('active');
+                    renderCompanyRows();
+                });
+
+            $('#review-company-search').off('input.auditAppendSearch').on('input.auditAppendSearch', function () {
+                var v = ($(this).val() || '').trim().toLowerCase();
+                if (reviewSearchTimer) {
+                    clearTimeout(reviewSearchTimer);
+                }
+                reviewSearchTimer = setTimeout(function () {
+                    reviewCompanySearchQ = v;
+                    renderCompanyRows();
+                }, 180);
+            });
+
+            $(document).off('change.auditAppendCb', '#review-company-tbody .review-company-cb')
+                .on('change.auditAppendCb', '#review-company-tbody .review-company-cb', function () {
+                    var $cb = $(this);
+                    if ($cb.prop('disabled')) {
+                        return;
+                    }
+                    var c = $cb.data('company');
+                    var k = procuremenReviewCompanyKey(c);
+                    var $cbs = $('#review-company-tbody .review-company-cb:not(:disabled)');
+                    var $master = $('#review-check-all');
+                    if (!k) {
+                        if (!$cbs.length) {
+                            $master.prop('checked', false).prop('indeterminate', false);
+                        } else {
+                            var n0 = $cbs.filter(':checked').length;
+                            $master.prop('checked', n0 === $cbs.length);
+                            $master.prop('indeterminate', n0 > 0 && n0 < $cbs.length);
+                        }
+                        syncReviewSelectedSummary();
+                        return;
+                    }
+                    if ($cb.prop('checked')) {
+                        reviewSelectedMap[k] = c;
+                    } else {
+                        delete reviewSelectedMap[k];
+                    }
+                    var n = $cbs.filter(':checked').length;
+                    $master.prop('checked', n === $cbs.length);
+                    $master.prop('indeterminate', n > 0 && n < $cbs.length);
+                    // 仅更新已选摘要,勿再钉高度(会按 offset 重算导致表格跳动)
+                    syncReviewSelectedSummary();
+                });
+
+            $('#review-check-all').off('change.auditAppendAll').on('change.auditAppendAll', function () {
+                var on = $(this).prop('checked');
+                $(this).prop('indeterminate', false);
+                $('#review-company-tbody .review-company-cb:not(:disabled)').each(function () {
+                    var $cb = $(this);
+                    var c = $cb.data('company');
+                    var k = procuremenReviewCompanyKey(c);
+                    $cb.prop('checked', on);
+                    if (!k) {
+                        return;
+                    }
+                    if (on) {
+                        reviewSelectedMap[k] = c;
+                    } else {
+                        delete reviewSelectedMap[k];
+                    }
+                });
+                syncReviewSelectedSummary();
+            });
+
+            $(document).off('click.auditAppendChip', '.review-chip-remove')
+                .on('click.auditAppendChip', '.review-chip-remove', function (e) {
+                    e.preventDefault();
+                    var k = $(this).closest('.review-selected-chip').data('reviewSelKey');
+                    if (k) {
+                        delete reviewSelectedMap[k];
+                    }
+                    renderCompanyRows();
+                });
+
+            function closeAppendLayer() {
+                var index = parent.Layer.getFrameIndex(window.name);
+                parent.Layer.close(index);
+            }
+
+            $('#btn-audit-append-close').off('click.auditAppendClose').on('click.auditAppendClose', function () {
+                closeAppendLayer();
+            });
+
+            $('#btn-audit-append-submit').off('click.auditAppendSubmit').on('click.auditAppendSubmit', function () {
+                var companies = [];
+                $.each(reviewSelectedMap, function (k, c) {
+                    if (!c || isExistingCompany(c)) {
+                        return;
+                    }
+                    companies.push(procuremenReviewCompanyPayload(c));
+                });
+                if (!companies.length) {
+                    Toastr.warning('请至少选择一家新的供应商');
+                    return;
+                }
+                // 弹窗久开后截止可能已到:确认前提示,服务端仍会再校验
+                var sysRq = ($('#audit-append-sys-rq').val() || '').trim();
+                if (sysRq) {
+                    var normalized = String(sysRq).replace(/T/, ' ').replace(/-/g, '/');
+                    var ts = Date.parse(normalized);
+                    if (ts && !isNaN(ts) && Date.now() >= ts) {
+                        Toastr.warning('已经超过了招标截止日期,不可操作');
+                        return;
+                    }
+                }
+                var $btn = $(this);
+                if ($btn.data('loading')) {
+                    return;
+                }
+                $btn.data('loading', 1).prop('disabled', true);
+                Fast.api.ajax({
+                    url: 'procuremen/auditappendsubmit',
+                    type: 'POST',
+                    data: {
+                        __token__: $('#audit-append-form input[name="__token__"]').val(),
+                        scydgy_id: $('#audit-append-scydgy-id').val(),
+                        companies_json: JSON.stringify(companies)
+                    }
+                }, function (data, ret) {
+                    var msg = (ret && ret.msg) ? ret.msg : '补加成功';
+                    if (typeof parent !== 'undefined' && parent.Toastr) {
+                        parent.Toastr.success(msg);
+                    } else {
+                        Toastr.success(msg);
+                    }
+                    setTimeout(function () {
+                        try {
+                            if (window.parent && window.parent !== window) {
+                                var frames = window.parent.frames;
+                                for (var fi = 0; fi < frames.length; fi++) {
+                                    try {
+                                        var href = frames[fi].location && frames[fi].location.href;
+                                        if (href && /auditissue/i.test(href)) {
+                                            frames[fi].location.reload();
+                                            break;
+                                        }
+                                    } catch (eF) {
+                                    }
+                                }
+                            }
+                        } catch (e2) {
+                        }
+                        closeAppendLayer();
+                    }, 350);
+                    return false;
+                }, function () {
+                    $btn.data('loading', 0).prop('disabled', false);
+                });
+            });
+
+            loadCompanies();
+        },
+
         auditissue: function () {
             function auditIssueClose() {
                 var index = parent.Layer.getFrameIndex(window.name);
@@ -3441,7 +4158,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                 $('#audit-sys-rq-wrap').find('.procuremen-sys-rq-date').trigger('change');
                 var sysRq = auditIssueSysRq();
                 if (!sysRq) {
-                    Toastr.warning('请选择报价截止时间');
+                    Toastr.warning('请选择招标截止日期');
                     procuremenFocusSysRqSplit($('#audit-sys-rq-wrap'));
                     return false;
                 }
@@ -3660,7 +4377,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                 var layerIdx = Layer.open({
                     type: 1,
                     title: '开标验证',
-                    area: ['560px', 'auto'],
+                    area: ['640px', 'auto'],
                     content: html,
                     btn: ['提交验证', '取消'],
                     yes: function (idx) {
@@ -3758,6 +4475,44 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                 auditIssueOpenBidModal();
             });
 
+            $('#btn-audit-append-supplier').off('click.auditAppend').on('click.auditAppend', function () {
+                var sid = $('input[name="scydgy_id"]').val() || '';
+                if (!sid) {
+                    Toastr.warning('缺少工序参数');
+                    return;
+                }
+                if (auditIssueBidOpenVerified()) {
+                    Toastr.warning('已开标验证,不能再补加供应商');
+                    return;
+                }
+                // 已过招标截止:按钮仍显示,点击提示不可操作
+                var deadlinePassed = parseInt($('#audit-append-deadline-passed').val(), 10) === 1;
+                if (!deadlinePassed) {
+                    var sysRq = auditIssueSysRq();
+                    if (sysRq) {
+                        var normalized = String(sysRq).replace(/T/, ' ').replace(/-/g, '/');
+                        var ts = Date.parse(normalized);
+                        if (ts && !isNaN(ts) && Date.now() >= ts) {
+                            deadlinePassed = true;
+                        }
+                    }
+                }
+                if (deadlinePassed) {
+                    Toastr.warning('已经超过了招标截止日期,不可操作');
+                    return;
+                }
+                var url = Fast.api.fixurl('procuremen/auditappend?scydgy_id=' + encodeURIComponent(sid));
+                var openApi = (typeof parent !== 'undefined' && parent.Backend && parent.Backend.api && parent.Backend.api.open)
+                    ? parent.Backend.api.open
+                    : (Backend.api.open.bind(Backend.api));
+                openApi(url, '补加供应商', {
+                    area: ['100%', '100%'],
+                    callback: function () {
+                        window.location.reload();
+                    }
+                });
+            });
+
             function auditIssueSelectedCompany() {
                 var $checked = $('input[name="audit_quote_pick"]:checked');
                 if (!$checked.length) {

Niektóre pliki nie zostały wyświetlone z powodu dużej ilości zmienionych plików