m0_70156489 hai 2 días
pai
achega
39814e4b5d

+ 219 - 102
application/admin/controller/Procuremen.php

@@ -1458,6 +1458,10 @@ class Procuremen extends Backend
                 unset($rw);
             }
 
+            if (in_array($wffTab, ['audit', 'confirm'], true) && count($rows) > 0) {
+                $this->enrichProcuremenListMilestoneTimes($rows);
+            }
+
             if ($wffTab === 'all' && count($rows) > 0) {
                 $this->mergePurchaseOrder($rows);
             }
@@ -2639,8 +2643,17 @@ class Procuremen extends Backend
 
         $name1 = $payload['verifier1_name'] !== '' ? $payload['verifier1_name'] : ('ID' . $payload['verifier1_id']);
         $name2 = $payload['verifier2_name'] !== '' ? $payload['verifier2_name'] : ('ID' . $payload['verifier2_id']);
-        $orderPart = '(订单' . $ccydh . ')';
-        $logMsg = '开标验证' . $orderPart . ':' . $name1 . '、' . $name2 . ' 于 ' . $now . ' 完成双重验证,可查看供应商报价信息';
+        $labelMap = ProcuremenOperLog::resolveAdminDisplayLabels([
+            (int)$payload['verifier1_id'],
+            (int)$payload['verifier2_id'],
+        ]);
+        if (isset($labelMap[(int)$payload['verifier1_id']]) && (int)$payload['verifier1_id'] > 0) {
+            $name1 = $labelMap[(int)$payload['verifier1_id']];
+        }
+        if (isset($labelMap[(int)$payload['verifier2_id']]) && (int)$payload['verifier2_id'] > 0) {
+            $name2 = $labelMap[(int)$payload['verifier2_id']];
+        }
+        $logMsg = '开标验证:' . $name1 . '、' . $name2 . ' 于 ' . $now . ' 完成双重验证';
 
         $seenSid = [];
         foreach ($bundle['pos'] ?? [] as $poRow) {
@@ -3853,10 +3866,14 @@ class Procuremen extends Backend
                 Log::write('完结存证PDF异常 scydgy_id=' . $ids . ' ' . $e->getMessage(), 'error');
             }
         } else {
+            $gymc = trim((string)($data['CGYMC'] ?? $row['CGYMC'] ?? ''));
+            $qtyPart = '本次数量「' . ($q !== '' ? $q : '') . '」';
+            $pricePart = '最高限价「' . ($p !== '' ? $p : '') . '」';
+            $detail = ($gymc !== '' ? ($gymc . $qtyPart) : $qtyPart) . ',' . $pricePart;
             $this->addOrderLog(
                 $ids,
                 'save_qty_price',
-                '保存本次数量、最高限价:本次数量「' . ($q !== '' ? $q : '') . '」,最高限价「' . ($p !== '' ? $p : '') . '」',
+                '保存本次数量、最高限价:' . $detail,
                 $poIdLog
             );
         }
@@ -4470,6 +4487,129 @@ class Procuremen extends Backend
         ProcuremenOperLog::write($scydgyId, $action, $content, $purchaseOrderId, $this->GetUseName());
     }
 
+    /**
+     * 列表页补充:下发 / 确定 / 审批时间(按当前页工序批量读操作日志)
+     *
+     * @param array<int, array<string, mixed>> $rows
+     */
+    protected function enrichProcuremenListMilestoneTimes(array &$rows): void
+    {
+        $sidList = [];
+        foreach ($rows as $rw) {
+            if (!is_array($rw)) {
+                continue;
+            }
+            if (!empty($rw['_order_merge_rows']) && is_array($rw['_order_merge_rows'])) {
+                foreach ($rw['_order_merge_rows'] as $mr) {
+                    $mid = (int)($mr['ID'] ?? 0);
+                    if ($this->isValidScydgyRowId($mid)) {
+                        $sidList[$mid] = true;
+                    }
+                }
+            }
+            $id = (int)($rw['ID'] ?? 0);
+            if ($this->isValidScydgyRowId($id)) {
+                $sidList[$id] = true;
+            }
+        }
+        $sids = array_keys($sidList);
+        $issueBySid = [];
+        $confirmBySid = [];
+        $approveBySid = [];
+        if ($sids !== []) {
+            $issueActs = ProcuremenOperLog::expandActionQueryValues('issue_submit');
+            $confirmActs = ProcuremenOperLog::expandActionQueryValues(['audit_select', 'audit_confirm']);
+            $approveActs = ProcuremenOperLog::expandActionQueryValues('purchase_confirm');
+            $allActs = array_values(array_unique(array_merge($issueActs, $confirmActs, $approveActs)));
+            try {
+                $logs = Db::table('purchase_order_oper_log')
+                    ->where(ProcuremenOperLog::COL_SCYDGY_ID, 'in', $sids)
+                    ->where(ProcuremenOperLog::COL_ACTION, 'in', $allActs)
+                    ->field(ProcuremenOperLog::COL_SCYDGY_ID . ',' . ProcuremenOperLog::COL_ACTION . ',' . ProcuremenOperLog::COL_TIME)
+                    ->order('id', 'asc')
+                    ->select();
+            } catch (\Throwable $e) {
+                $logs = [];
+            }
+            if (is_array($logs)) {
+                $issueSet = array_fill_keys($issueActs, true);
+                $confirmSet = array_fill_keys($confirmActs, true);
+                $approveSet = array_fill_keys($approveActs, true);
+                foreach ($logs as $lg) {
+                    if (!is_array($lg)) {
+                        continue;
+                    }
+                    $lg = ProcuremenOperLog::normalizeRow($lg);
+                    $sid = (int)($lg[ProcuremenOperLog::COL_SCYDGY_ID] ?? 0);
+                    if ($sid === 0) {
+                        continue;
+                    }
+                    $action = trim((string)($lg[ProcuremenOperLog::COL_ACTION] ?? ''));
+                    $t = $this->formatProcuremenDetailTime($lg[ProcuremenOperLog::COL_TIME] ?? null);
+                    if ($t === '') {
+                        continue;
+                    }
+                    if (isset($issueSet[$action])) {
+                        if (!isset($issueBySid[$sid]) || strcmp($t, $issueBySid[$sid]) > 0) {
+                            $issueBySid[$sid] = $t;
+                        }
+                    }
+                    if (isset($confirmSet[$action])) {
+                        if (!isset($confirmBySid[$sid]) || strcmp($t, $confirmBySid[$sid]) > 0) {
+                            $confirmBySid[$sid] = $t;
+                        }
+                    }
+                    if (isset($approveSet[$action])) {
+                        if (!isset($approveBySid[$sid]) || strcmp($t, $approveBySid[$sid]) > 0) {
+                            $approveBySid[$sid] = $t;
+                        }
+                    }
+                }
+            }
+        }
+
+        foreach ($rows as &$rw) {
+            if (!is_array($rw)) {
+                continue;
+            }
+            $mergeSids = [];
+            if (!empty($rw['_order_merge_rows']) && is_array($rw['_order_merge_rows'])) {
+                foreach ($rw['_order_merge_rows'] as $mr) {
+                    $mid = (int)($mr['ID'] ?? 0);
+                    if ($this->isValidScydgyRowId($mid)) {
+                        $mergeSids[] = $mid;
+                    }
+                }
+            }
+            $rid = (int)($rw['ID'] ?? 0);
+            if ($this->isValidScydgyRowId($rid) && !in_array($rid, $mergeSids, true)) {
+                $mergeSids[] = $rid;
+            }
+            $pick = $this->formatProcuremenDetailTime($rw['pick_time'] ?? null);
+            $issue = $pick;
+            $confirm = '';
+            $approve = '';
+            foreach ($mergeSids as $sid) {
+                if (isset($issueBySid[$sid]) && ($issue === '' || strcmp($issueBySid[$sid], $issue) > 0)) {
+                    $issue = $issueBySid[$sid];
+                }
+                if (isset($confirmBySid[$sid]) && ($confirm === '' || strcmp($confirmBySid[$sid], $confirm) > 0)) {
+                    $confirm = $confirmBySid[$sid];
+                }
+                if (isset($approveBySid[$sid]) && ($approve === '' || strcmp($approveBySid[$sid], $approve) > 0)) {
+                    $approve = $approveBySid[$sid];
+                }
+            }
+            if ($issue === '' && $pick !== '') {
+                $issue = $pick;
+            }
+            $rw['issue_time'] = $issue;
+            $rw['confirm_time'] = $confirm;
+            $rw['approve_time'] = $approve;
+        }
+        unset($rw);
+    }
+
     /**
      * 按主键取一条
      */
@@ -5059,7 +5199,18 @@ class Procuremen extends Backend
         if (!is_array($operLogs)) {
             $operLogs = [];
         }
-        $operLogs = ProcuremenOperLog::formatLogRowsForDisplay($operLogs);
+        $processNameBySid = [];
+        foreach ($mergeRows as $mr) {
+            if (!is_array($mr)) {
+                continue;
+            }
+            $psid = (int)($mr['ID'] ?? $mr['scydgy_id'] ?? 0);
+            $gymc = trim((string)($mr['CGYMC'] ?? ''));
+            if ($psid !== 0 && $gymc !== '') {
+                $processNameBySid[$psid] = $gymc;
+            }
+        }
+        $operLogs = ProcuremenOperLog::formatLogRowsForDisplay($operLogs, $processNameBySid);
 
         $bundle = $this->buildProcuremenDetailsViewData($main, $activeDetails);
         $quoteView = $this->buildProcuremenDetailsQuoteView($mergeRows, $details, $main);
@@ -7559,19 +7710,8 @@ class Procuremen extends Backend
         );
 
         $isMerge = count($mergeRows) > 1;
-        $ccydhLog = '';
-        foreach ($mergeRows as $row) {
-            if (!is_array($row)) {
-                continue;
-            }
-            $ccydhLog = trim((string)($row['CCYDH'] ?? ''));
-            if ($ccydhLog !== '') {
-                break;
-            }
-        }
         $procPart = $isMerge ? '(' . count($mergeRows) . ' 道工序)' : '';
-        $orderPart = $ccydhLog !== '' ? '(订单' . $ccydhLog . ')' : '';
-        $logMsg = '下发' . $orderPart . $procPart
+        $logMsg = '下发' . $procPart
             . '通知供应商(' . count($issueCompanies) . ' 家):' . $issueNamesSummary;
 
         $issueTime = date('Y-m-d H:i:s');
@@ -8221,11 +8361,9 @@ class Procuremen extends Backend
             $this->error('请先完成开标双重验证后再确认供应商');
         }
 
-        $ccydhLog = trim((string)($bundle['ccydh'] ?? ''));
         $procCnt = count($mergeRows);
-        $orderPart = $ccydhLog !== '' ? '(订单' . $ccydhLog . ')' : '';
         $procPart = $procCnt > 1 ? '(' . $procCnt . ' 道工序)' : '';
-        $logMsg = '确认供应商' . $orderPart . $procPart . ':已选定「' . $companyName . '」';
+        $logMsg = '确认供应商' . $procPart . ':已选定「' . $companyName . '」';
 
         Db::startTrans();
         try {
@@ -8335,6 +8473,7 @@ class Procuremen extends Backend
             return [];
         }
         $sids = [];
+        $poIdBySid = [];
         foreach ($pos as $poRow) {
             if (!is_array($poRow)) {
                 continue;
@@ -8342,14 +8481,17 @@ class Procuremen extends Backend
             $sid = (int)($poRow['scydgy_id'] ?? 0);
             if ($this->isValidScydgyRowId($sid)) {
                 $sids[$sid] = true;
+                $poId = (int)($poRow['id'] ?? $poRow['ID'] ?? 0);
+                if ($poId > 0) {
+                    $poIdBySid[$sid] = $poId;
+                }
             }
         }
         if ($sids === []) {
             return [];
         }
 
-        $ccydhLog = trim((string)($bundle['ccydh'] ?? ''));
-        $logMsg = '重新下发' . ($ccydhLog !== '' ? '(' . $ccydhLog . ')' : '');
+        $logMsg = '重新下发';
         $suffix = trim((string)$logSuffix, ',, ');
         if ($suffix !== '' && mb_strpos($suffix, '历史存证', 0, 'UTF-8') !== false) {
             $logMsg .= ':从历史存证退回协助初选';
@@ -8370,7 +8512,8 @@ class Procuremen extends Backend
         $this->voidPurchaseOrderDetailsForScydgyIds(array_keys($sids));
 
         foreach (array_keys($sids) as $sid) {
-            $this->addOrderLog((int)$sid, $logAction, $logMsg, null);
+            $poIdLog = (int)($poIdBySid[$sid] ?? 0);
+            $this->addOrderLog((int)$sid, $logAction, $logMsg, $poIdLog > 0 ? $poIdLog : null);
         }
 
         return $sids;
@@ -8420,19 +8563,52 @@ class Procuremen extends Backend
 
     protected function purchaseOrderRowIsCompleted(array $po): bool
     {
-        $st = $po['status'] ?? null;
-        if ($st === 1 || $st === '1') {
-            return true;
+        return ProcuremenStatus::isPoCompleted($po['status'] ?? '');
+    }
+
+    /**
+     * 重新下发 — 按工序 ID 加载同单全部主单(不限状态;点了重新下发即可退回)
+     *
+     * @return array{ccydh:string, pos:array<int,array>, merge_rows:array<int,array>}
+     */
+    protected function loadAnyOrderBundleByScydgyId(string $scydgyId): array
+    {
+        $scydgyId = trim($scydgyId);
+        $empty = ['ccydh' => '', 'pos' => [], 'merge_rows' => []];
+        if ($scydgyId === '') {
+            return $empty;
         }
-        if (is_string($st) && trim($st) === '1') {
-            return true;
+        try {
+            $anchor = Db::table('purchase_order')->where('scydgy_id', $scydgyId)->find();
+        } catch (\Throwable $e) {
+            $anchor = null;
+        }
+        if (!is_array($anchor)) {
+            return $empty;
+        }
+        $ccydh = trim((string)($anchor['CCYDH'] ?? ''));
+        if ($ccydh === '') {
+            return ['ccydh' => '', 'pos' => [$anchor], 'merge_rows' => $this->scydgyRowsForPurchaseOrders([$anchor])];
+        }
+        try {
+            $pos = Db::table('purchase_order')->where('CCYDH', $ccydh)->order('scydgy_id', 'asc')->select();
+        } catch (\Throwable $e) {
+            $pos = [$anchor];
+        }
+        if (!is_array($pos) || $pos === []) {
+            $pos = [$anchor];
         }
 
-        return is_numeric($st) && (int)$st === 1;
+        return [
+            'ccydh'      => $ccydh,
+            'pos'        => $pos,
+            'merge_rows' => $this->scydgyRowsForPurchaseOrders($pos),
+        ];
     }
 
     /**
      * 废弃订单 — 退回协助初选重新询价(支持单条或批量)
+     * 统一入口:待确认 / 已完结 / 其它状态均可重新下发
      */
     public function auditAbandon()
     {
@@ -8470,7 +8646,16 @@ class Procuremen extends Backend
         Db::startTrans();
         try {
             foreach ($anchorIds as $anchorId) {
+                $fromArchive = false;
                 $bundle = $this->loadAuditOrderBundleByScydgyId($anchorId);
+                if (($bundle['pos'] ?? []) === []) {
+                    $bundle = $this->loadArchiveOrderBundleByScydgyId($anchorId);
+                    $fromArchive = ($bundle['pos'] ?? []) !== [];
+                }
+                if (($bundle['pos'] ?? []) === []) {
+                    $bundle = $this->loadAnyOrderBundleByScydgyId($anchorId);
+                    $fromArchive = ($bundle['pos'] ?? []) !== [];
+                }
                 if (($bundle['pos'] ?? []) === []) {
                     continue;
                 }
@@ -8480,7 +8665,9 @@ class Procuremen extends Backend
                     continue;
                 }
                 $processedCcydh[$dedupeKey] = true;
-                $sids = $this->abandonOrderBundleToPick($bundle, 'audit_abandon');
+                $sids = $fromArchive
+                    ? $this->abandonOrderBundleToPick($bundle, 'audit_abandon', ',从历史存证退回协助初选')
+                    : $this->abandonOrderBundleToPick($bundle, 'audit_abandon');
                 if ($sids === []) {
                     continue;
                 }
@@ -8490,7 +8677,7 @@ class Procuremen extends Backend
                 }
             }
             if ($doneOrders < 1) {
-                throw new \Exception('所选订单均不在待确认供应商状态');
+                throw new \Exception('未找到可重新下发的订单,请刷新后重试');
             }
             Db::commit();
         } catch (\Throwable $e) {
@@ -8507,81 +8694,11 @@ class Procuremen extends Backend
     }
 
     /**
-     * 历史存证 — 已完结订单重新下发,退回协助初选(支持单条或批量
+     * @deprecated 请统一使用 auditAbandon(procuremen/auditabandon
      */
     public function archiveAbandon()
     {
-        if (!$this->request->isPost()) {
-            $this->error(__('Invalid parameters'));
-        }
-        if (!$this->hasProcuremenPerm(['archiveabandon', 'archiveAbandon', 'auditabandon', 'auditAbandon'])) {
-            $this->error(__('You have no permission'));
-        }
-
-        $anchorIds = [];
-        $batchRaw = trim((string)$this->request->post('scydgy_ids_json', ''));
-        if ($batchRaw !== '') {
-            $decoded = json_decode($batchRaw, true);
-            if (is_array($decoded)) {
-                foreach ($decoded as $id) {
-                    $s = trim((string)$id);
-                    if ($s !== '') {
-                        $anchorIds[] = $s;
-                    }
-                }
-            }
-        }
-        if ($anchorIds === []) {
-            $single = trim((string)$this->request->post('scydgy_id', ''));
-            if ($single !== '') {
-                $anchorIds[] = $single;
-            }
-        }
-        if ($anchorIds === []) {
-            $this->error('请至少选择一条订单');
-        }
-
-        $processedCcydh = [];
-        $doneOrders = 0;
-
-        Db::startTrans();
-        try {
-            foreach ($anchorIds as $anchorId) {
-                $bundle = $this->loadArchiveOrderBundleByScydgyId($anchorId);
-                if (($bundle['pos'] ?? []) === []) {
-                    continue;
-                }
-                $ccydh = trim((string)($bundle['ccydh'] ?? ''));
-                $dedupeKey = $ccydh !== '' ? $ccydh : ('sid:' . $anchorId);
-                if (isset($processedCcydh[$dedupeKey])) {
-                    continue;
-                }
-                $processedCcydh[$dedupeKey] = true;
-                $sids = $this->abandonOrderBundleToPick(
-                    $bundle,
-                    'archive_abandon',
-                    ',从历史存证退回协助初选'
-                );
-                if ($sids === []) {
-                    continue;
-                }
-                $doneOrders++;
-            }
-            if ($doneOrders < 1) {
-                throw new \Exception('所选订单均不是已完结状态');
-            }
-            Db::commit();
-        } catch (\Throwable $e) {
-            Db::rollback();
-            $this->error($e->getMessage());
-        }
-
-        $this->pickHiddenScydgySetCache = null;
-
-        $msg = $doneOrders > 1
-            ? ('已重新下发 ' . $doneOrders . ' 个订单并退回协助初选,历史记录已保留')
-            : '已退回协助初选,请重新选择供应商下发(历史记录已保留)';
-        $this->success($msg);
+        return $this->auditAbandon();
     }
 
     /**

+ 4 - 3
application/admin/controller/Procuremenarchive.php

@@ -24,20 +24,21 @@ class Procuremenarchive extends Backend
         $this->view->assign('canArchiveAbandon', $canArchiveAbandon);
         $this->assignconfig('procuremenAuth', [
             'details'        => $canDetails,
+            'auditabandon'   => $canArchiveAbandon,
             'archiveabandon' => $canArchiveAbandon,
         ]);
     }
 
     /**
-     * 历史存证「重新下发」权限(规则名在 procuremen 控制器下)
+     * 历史存证「重新下发」权限:与审批列表统一为 auditabandon
      */
     protected function canArchiveAbandon(): bool
     {
-        if ($this->hasProcuremenSiblingPerm(['archiveabandon', 'archiveAbandon'])) {
+        if ($this->hasProcuremenSiblingPerm(['auditabandon', 'auditAbandon'])) {
             return true;
         }
 
-        return $this->hasProcuremenSiblingPerm(['auditabandon', 'auditAbandon']);
+        return $this->hasProcuremenSiblingPerm(['archiveabandon', 'archiveAbandon']);
     }
 
     /**

+ 3 - 10
application/admin/controller/Procuremenmenu.php

@@ -158,20 +158,13 @@ class Procuremenmenu extends Backend
                 }
             }
             if ($m['name'] === 'procuremenarchive/index') {
+                // 重新下发与审批统一用 procuremen/auditabandon,已存在则不挪父级
                 $archiveChildren = [
-                    ['name' => 'procuremen/archiveabandon', 'title' => '重新下发'],
+                    ['name' => 'procuremen/auditabandon', 'title' => '重新下发'],
                     ['name' => 'procuremen/details', 'title' => '详情'],
                 ];
                 foreach ($archiveChildren as $cr) {
-                    $exists = Db::name('auth_rule')->where('name', $cr['name'])->find();
-                    if ($exists) {
-                        Db::name('auth_rule')->where('name', $cr['name'])->update([
-                            'pid'        => $menuId,
-                            'title'      => $cr['title'],
-                            'status'     => 'normal',
-                            'ismenu'     => 0,
-                            'updatetime' => $t,
-                        ]);
+                    if (Db::name('auth_rule')->where('name', $cr['name'])->find()) {
                         continue;
                     }
                     Db::name('auth_rule')->insert([

+ 1 - 1
application/admin/view/dashboard/index.html

@@ -754,7 +754,7 @@
                         <div class="side-col">
                             <div class="proc-summary-panel proc-rank-panel">
                                 <div class="rank-head">
-                                    <span class="rank-title"><i class="fa fa-bar-chart"></i> 供应商排名</span>
+                                    <span class="rank-title"><i class="fa fa-bar-chart"></i> 供应商排名(本月)</span>
                                     <span class="rank-supplier-inline">供应商总数 <strong>{$supplierRank.supplier_total|default='0'}</strong></span>
                                 </div>
                                 <ul class="nav nav-tabs rank-tabs">

+ 1 - 1
application/admin/view/index/login.html

@@ -636,7 +636,7 @@
         </div>
 
         <div class="login-brand-footer">
-            Copyright @ 杭州可集达科技有限公司 2026-{:date('Y',time())} 版权所有
+            Copyright @ 杭州可集达科技有限公司提供技术支持
             {if $site.beian}<a href="https://beian.miit.gov.cn" target="_blank"> {$site.beian|htmlentities}</a>{/if}
         </div>
     </section>

+ 22 - 5
application/admin/view/procuremen/details_fragment.html

@@ -88,6 +88,8 @@
         line-height: 1.35;
         margin-bottom: 2px;
         flex: 0 0 auto;
+        text-align: center;
+        width: 100%;
     }
     .proc-step-sub {
         font-size: 11px;
@@ -97,7 +99,7 @@
         flex: 1 1 auto;
         white-space: pre-line;
         word-break: break-word;
-        text-align: left;
+        text-align: center;
         width: 100%;
         max-width: 100%;
         box-sizing: border-box;
@@ -109,7 +111,7 @@
         padding-top: 6px;
         white-space: pre-line;
         line-height: 1.35;
-        text-align: left;
+        text-align: center;
         width: 100%;
         max-width: 100%;
         box-sizing: border-box;
@@ -205,11 +207,14 @@
         border: 1px solid #e8e8e8;
         border-radius: 4px;
         background: #fafafa;
-        /* 记录多时全部展开,由详情弹窗整体滚动 */
         max-height: none;
         overflow: visible;
     }
     .procuremen-details-wrap .procuremen-oper-log li {
+        display: grid;
+        grid-template-columns: 7.5em 10em 8em minmax(0, 1fr);
+        column-gap: 6px;
+        align-items: start;
         padding: 8px 12px;
         border-bottom: 1px solid #eee;
         word-break: break-word;
@@ -220,13 +225,24 @@
     .procuremen-details-wrap .procuremen-oper-log .op-user {
         font-weight: 700;
         color: #000;
+        white-space: nowrap;
+        overflow: hidden;
+        text-overflow: ellipsis;
     }
     .procuremen-details-wrap .procuremen-oper-log .op-time {
         color: #666;
-        margin: 0 6px;
+        margin: 0;
+        white-space: nowrap;
+        font-variant-numeric: tabular-nums;
+    }
+    .procuremen-details-wrap .procuremen-oper-log .op-action {
+        font-weight: normal;
+        color: #303133;
+        white-space: nowrap;
     }
     .procuremen-details-wrap .procuremen-oper-log .op-text {
         color: #333;
+        min-width: 0;
     }
     .procuremen-details-wrap .procuremen-oper-log-empty {
         font-size: 13px;
@@ -426,7 +442,7 @@
                         <td style="vertical-align:top;padding:2px 2px 0;border:0 !important;font-size:12px;color:#333;line-height:1.35;font-weight:600;text-align:center;">{$st.title|htmlentities}</td>
                     </tr>
                     <tr>
-                        <td style="vertical-align:top;padding:2px 2px 0;height:72px;border:0 !important;font-size:11px;color:#888;line-height:1.45;text-align:left;overflow:hidden;">{$st.subtitle|default=''|htmlentities|nl2br}</td>
+                        <td style="vertical-align:top;padding:2px 2px 0;height:72px;border:0 !important;font-size:11px;color:#888;line-height:1.45;text-align:center;overflow:hidden;">{$st.subtitle|default=''|htmlentities|nl2br}</td>
                     </tr>
                     <tr>
                         <td style="vertical-align:bottom;padding:2px 2px 0;border:0 !important;font-size:11px;color:#aaa;text-align:center;line-height:1.3;">{$st.time|default=''|htmlentities|nl2br}</td>
@@ -463,6 +479,7 @@
         <li>
             <span class="op-user">{$lg.admin_name|default=''|htmlentities}</span>
             <span class="op-time">{$lg.createtime_text|default=''|htmlentities}</span>
+            <span class="op-action">{notempty name="lg.action_text"}{$lg.action_text|htmlentities}{else /}{$lg.action|default='操作'|htmlentities}{/notempty}:</span>
             <span class="op-text">{$lg.content|default=''|htmlentities}</span>
         </li>
         {/volist}

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

@@ -654,10 +654,16 @@
     /* 日期列加宽并禁止省略,避免滚到最右仍被竖向滚动条挡住 */
     #procuremen-layout .bootstrap-table thead th[data-field="dStamp"],
     #procuremen-layout .bootstrap-table thead th[data-field="dputrecord"],
+    #procuremen-layout .bootstrap-table thead th[data-field="issue_time"],
+    #procuremen-layout .bootstrap-table thead th[data-field="confirm_time"],
+    #procuremen-layout .bootstrap-table thead th[data-field="approve_time"],
     #procuremen-layout .bootstrap-table tbody td[data-field="dStamp"],
-    #procuremen-layout .bootstrap-table tbody td[data-field="dputrecord"] {
-        min-width: 176px !important;
-        width: 176px !important;
+    #procuremen-layout .bootstrap-table tbody td[data-field="dputrecord"],
+    #procuremen-layout .bootstrap-table tbody td[data-field="issue_time"],
+    #procuremen-layout .bootstrap-table tbody td[data-field="confirm_time"],
+    #procuremen-layout .bootstrap-table tbody td[data-field="approve_time"] {
+        min-width: 160px !important;
+        width: 160px !important;
         max-width: 176px !important;
         white-space: nowrap;
         overflow: hidden;

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

@@ -239,9 +239,10 @@ class ProcuremenOperLog
 
     /**
      * @param array<int, array<string, mixed>> $logs
+     * @param array<int, string> $processNameBySid scydgy_id => 工序名称
      * @return array<int, array<string, mixed>>
      */
-    public static function formatLogRowsForDisplay(array $logs): array
+    public static function formatLogRowsForDisplay(array $logs, array $processNameBySid = []): array
     {
         foreach ($logs as &$lg) {
             if (!is_array($lg)) {
@@ -253,12 +254,546 @@ class ProcuremenOperLog
                 (string)($lg['content'] ?? ''),
                 (string)($lg['action'] ?? '')
             );
+            $lg['action_text'] = self::toActionLabel((string)($lg['action'] ?? ''));
+            $lg = self::enrichSaveQtyPriceProcessName($lg, $processNameBySid);
+        }
+        unset($lg);
+
+        $logs = self::mergeDuplicateProcessLogs($logs, $processNameBySid);
+        $logs = self::enrichAdminDisplayFields($logs);
+
+        foreach ($logs as &$lg) {
+            if (!is_array($lg)) {
+                continue;
+            }
+            $lg = self::splitActionAndDetailForDisplay($lg);
         }
         unset($lg);
 
         return $logs;
     }
 
+    /**
+     * 展示拆成「操作类型」+「操作说明」,说明里不再重复类型前缀
+     *
+     * @param array<string, mixed> $lg
+     * @return array<string, mixed>
+     */
+    protected static function splitActionAndDetailForDisplay(array $lg): array
+    {
+        $action = trim((string)($lg['action_text'] ?? ''));
+        if ($action === '') {
+            $action = self::toActionLabel((string)($lg[self::COL_ACTION] ?? ''));
+        }
+        $content = trim((string)($lg[self::COL_CONTENT] ?? ''));
+        $content = self::stripLeadingActionPrefixes($content, $action);
+        if ($action === '') {
+            $action = '操作';
+        }
+        $lg['action_text'] = $action;
+        $lg[self::COL_CONTENT] = $content;
+
+        return $lg;
+    }
+
+    /**
+     * @return string[] 按长度降序
+     */
+    protected static function contentActionPrefixes(string $action): array
+    {
+        $action = self::toActionLabel(trim($action));
+        $list = [];
+        if ($action !== '') {
+            $list[] = $action;
+        }
+        // 历史/文案别名
+        $aliases = [
+            '下发通知'       => ['下发', '下发通知'],
+            '保存数量限价'   => ['保存数量限价', '保存本次数量、最高限价'],
+            '审批重新下发'   => ['审批重新下发', '重新下发'],
+            '历史重新下发'   => ['历史重新下发', '存证重新下发', '重新下发'],
+            '审核确认供应商' => ['审核确认供应商', '采购确认'],
+            '确认供应商'     => ['确认供应商'],
+            '开标验证'       => ['开标验证'],
+            '补加供应商'     => ['补加供应商'],
+            '直接完结'       => ['直接完结', '完结'],
+            '审核驳回'       => ['审核驳回', '采购终审驳回'],
+        ];
+        if (isset($aliases[$action])) {
+            foreach ($aliases[$action] as $a) {
+                $list[] = $a;
+            }
+        }
+        $list = array_values(array_unique(array_filter(array_map('trim', $list))));
+        usort($list, static function ($a, $b) {
+            return mb_strlen((string)$b, 'UTF-8') <=> mb_strlen((string)$a, 'UTF-8');
+        });
+
+        return $list;
+    }
+
+    protected static function stripLeadingActionPrefixes(string $content, string $action): string
+    {
+        $content = trim($content);
+        if ($content === '') {
+            return '';
+        }
+        foreach (self::contentActionPrefixes($action) as $p) {
+            foreach ([$p . ':', $p . ':'] as $pref) {
+                if (mb_strpos($content, $pref, 0, 'UTF-8') === 0) {
+                    return trim(mb_substr($content, mb_strlen($pref, 'UTF-8'), null, 'UTF-8'));
+                }
+            }
+        }
+
+        return $content;
+    }
+
+    /**
+     * 历史「保存数量限价」说明补工序名:…:压折线本次数量「330」,最高限价「10」
+     *
+     * @param array<string, mixed> $lg
+     * @param array<int, string> $processNameBySid
+     * @return array<string, mixed>
+     */
+    protected static function enrichSaveQtyPriceProcessName(array $lg, array $processNameBySid): array
+    {
+        $action = trim((string)($lg['action_text'] ?? $lg[self::COL_ACTION] ?? ''));
+        if ($action !== '保存数量限价' && $action !== 'save_qty_price') {
+            return $lg;
+        }
+        $content = trim((string)($lg[self::COL_CONTENT] ?? ''));
+        if ($content === '') {
+            return $lg;
+        }
+        $sid = (int)($lg[self::COL_SCYDGY_ID] ?? 0);
+        $proc = trim((string)($processNameBySid[$sid] ?? ''));
+        if ($proc === '') {
+            return $lg;
+        }
+        // 已含工序名则不重复
+        if (mb_strpos($content, $proc . '本次数量', 0, 'UTF-8') !== false
+            || mb_strpos($content, '(' . $proc . ')', 0, 'UTF-8') !== false) {
+            return $lg;
+        }
+        if (mb_strpos($content, ':本次数量', 0, 'UTF-8') !== false) {
+            $lg[self::COL_CONTENT] = preg_replace('/:本次数量/u', ':' . $proc . '本次数量', $content, 1);
+        } elseif (mb_strpos($content, ':本次数量', 0, 'UTF-8') !== false) {
+            $lg[self::COL_CONTENT] = preg_replace('/:本次数量/u', ':' . $proc . '本次数量', $content, 1);
+        }
+
+        return $lg;
+    }
+
+    /**
+     * 操作人前加所属组别;说明文案中的人员名同样补组别
+     *
+     * @param array<int, array<string, mixed>> $logs
+     * @return array<int, array<string, mixed>>
+     */
+    public static function enrichAdminDisplayFields(array $logs): array
+    {
+        $ids = [];
+        foreach ($logs as $lg) {
+            if (!is_array($lg)) {
+                continue;
+            }
+            $id = (int)($lg[self::COL_ADMIN_ID] ?? 0);
+            if ($id > 0) {
+                $ids[$id] = true;
+            }
+        }
+        $map = self::loadAdminGroupNicknameMap(array_keys($ids));
+        // 仅用于纠正历史误伤:说明里曾把「组别 昵称」拼进供应商名,展示时还原为昵称/公司名
+        $nameToLabeled = self::buildAdminNameLabelMap(self::loadAllAdminGroupNicknameMap());
+        foreach (self::buildAdminNameLabelMap($map) as $name => $labeled) {
+            $nameToLabeled[$name] = $labeled;
+        }
+        uksort($nameToLabeled, static function ($a, $b) {
+            return mb_strlen((string)$b, 'UTF-8') <=> mb_strlen((string)$a, 'UTF-8');
+        });
+
+        foreach ($logs as &$lg) {
+            if (!is_array($lg)) {
+                continue;
+            }
+            $id = (int)($lg[self::COL_ADMIN_ID] ?? 0);
+            $info = ($id > 0 && isset($map[$id])) ? $map[$id] : null;
+            $nick = is_array($info) ? trim((string)($info['nickname'] ?? '')) : '';
+            if ($nick === '') {
+                $nick = trim((string)($lg[self::COL_ADMIN_NAME] ?? ''));
+            }
+            if ($nick === '') {
+                $nick = '未知用户';
+            }
+            $group = is_array($info) ? trim((string)($info['group_name'] ?? '')) : '';
+            $lg['admin_group'] = $group !== '' ? $group : '—';
+            $lg['admin_nickname'] = $nick;
+            // 操作人显示「组别 昵称」
+            $lg[self::COL_ADMIN_NAME] = self::formatAdminWithGroup($group, $nick);
+            $content = (string)($lg[self::COL_CONTENT] ?? '');
+            if ($content !== '' && $nameToLabeled !== []) {
+                // 先还原历史误伤的供应商名
+                $content = self::unprefixMistakenNamesInText($content, $nameToLabeled);
+                // 开标验证等说明里是人员名,再补组别
+                $actionLabel = self::toActionLabel((string)($lg['action_text'] ?? $lg[self::COL_ACTION] ?? ''));
+                if (self::actionContentNeedsPersonGroup($actionLabel)) {
+                    $content = self::prefixNamesInText($content, $nameToLabeled);
+                }
+                $lg[self::COL_CONTENT] = $content;
+            }
+        }
+        unset($lg);
+
+        return $logs;
+    }
+
+    /** 操作说明里会出现人员姓名、需补所属组别的类型 */
+    protected static function actionContentNeedsPersonGroup(string $action): bool
+    {
+        $action = self::toActionLabel(trim($action));
+
+        return $action === '开标验证';
+    }
+
+    /**
+     * 按 admin.id 解析展示名「组别 昵称」
+     *
+     * @param array<int, int> $adminIds
+     * @return array<int, string>
+     */
+    public static function resolveAdminDisplayLabels(array $adminIds): array
+    {
+        $map = self::loadAdminGroupNicknameMap($adminIds);
+        $out = [];
+        foreach ($adminIds as $rawId) {
+            $id = (int)$rawId;
+            if ($id <= 0) {
+                continue;
+            }
+            $info = isset($map[$id]) && is_array($map[$id]) ? $map[$id] : null;
+            $nick = is_array($info) ? trim((string)($info['nickname'] ?? '')) : '';
+            $group = is_array($info) ? trim((string)($info['group_name'] ?? '')) : '';
+            if ($nick === '') {
+                $nick = 'ID' . $id;
+            }
+            $out[$id] = self::formatAdminWithGroup($group, $nick);
+        }
+
+        return $out;
+    }
+
+    /**
+     * @param array<int, array{nickname:string,group_name:string}> $adminMap
+     * @return array<string, string> 原名 => 「组别 名称」
+     */
+    protected static function buildAdminNameLabelMap(array $adminMap): array
+    {
+        $out = [];
+        foreach ($adminMap as $info) {
+            if (!is_array($info)) {
+                continue;
+            }
+            $nick = trim((string)($info['nickname'] ?? ''));
+            if ($nick === '') {
+                continue;
+            }
+            $group = trim((string)($info['group_name'] ?? ''));
+            $labeled = self::formatAdminWithGroup($group, $nick);
+            if ($labeled !== $nick) {
+                $out[$nick] = $labeled;
+            }
+        }
+        // 长名优先,避免短名误伤
+        uksort($out, static function ($a, $b) {
+            return mb_strlen((string)$b, 'UTF-8') <=> mb_strlen((string)$a, 'UTF-8');
+        });
+
+        return $out;
+    }
+
+    protected static function formatAdminWithGroup(string $group, string $name): string
+    {
+        $name = trim($name);
+        $group = trim($group);
+        if ($name === '') {
+            return $group !== '' ? $group : '未知用户';
+        }
+        if ($group === '' || $group === '—' || $group === $name) {
+            return $name;
+        }
+        // 已带组别则不再重复
+        if (mb_strpos($name, $group, 0, 'UTF-8') === 0) {
+            return $name;
+        }
+
+        return $group . ' ' . $name;
+    }
+
+    /**
+     * @param array<string, string> $nameToLabeled
+     */
+    protected static function prefixNamesInText(string $text, array $nameToLabeled): string
+    {
+        foreach ($nameToLabeled as $name => $labeled) {
+            if ($name === '' || $labeled === '' || $name === $labeled) {
+                continue;
+            }
+            if (mb_strpos($text, $labeled, 0, 'UTF-8') !== false) {
+                continue;
+            }
+            if (mb_strpos($text, $name, 0, 'UTF-8') === false) {
+                continue;
+            }
+            $text = str_replace($name, $labeled, $text);
+        }
+
+        return $text;
+    }
+
+    /**
+     * 还原说明里误拼的「组别 昵称」→ 原名(避免供应商名被改成「浙江新华管理员 新华印刷…」)
+     *
+     * @param array<string, string> $nameToLabeled
+     */
+    protected static function unprefixMistakenNamesInText(string $text, array $nameToLabeled): string
+    {
+        // 长标签优先替换
+        $pairs = [];
+        foreach ($nameToLabeled as $name => $labeled) {
+            if ($name === '' || $labeled === '' || $name === $labeled) {
+                continue;
+            }
+            $pairs[$labeled] = $name;
+        }
+        uksort($pairs, static function ($a, $b) {
+            return mb_strlen((string)$b, 'UTF-8') <=> mb_strlen((string)$a, 'UTF-8');
+        });
+        foreach ($pairs as $labeled => $name) {
+            if (mb_strpos($text, $labeled, 0, 'UTF-8') === false) {
+                continue;
+            }
+            $text = str_replace($labeled, $name, $text);
+        }
+
+        return $text;
+    }
+
+    /**
+     * @return array<int, array{nickname:string,group_name:string,username?:string}>
+     */
+    public static function loadAllAdminGroupNicknameMap(): array
+    {
+        try {
+            $ids = Db::name('admin')->column('id');
+        } catch (\Throwable $e) {
+            $ids = [];
+        }
+        if (!is_array($ids) || $ids === []) {
+            return [];
+        }
+
+        return self::loadAdminGroupNicknameMap(array_map('intval', $ids));
+    }
+
+    /**
+     * @param array<int, int> $adminIds
+     * @return array<int, array{nickname:string,group_name:string,username?:string}>
+     */
+    public static function loadAdminGroupNicknameMap(array $adminIds): array
+    {
+        $out = [];
+        $adminIds = array_values(array_filter(array_map('intval', $adminIds)));
+        if ($adminIds === []) {
+            return $out;
+        }
+        try {
+            $admins = Db::name('admin')->where('id', 'in', $adminIds)->field('id,nickname,username')->select();
+        } catch (\Throwable $e) {
+            $admins = [];
+        }
+        if (is_array($admins)) {
+            foreach ($admins as $a) {
+                if (!is_array($a)) {
+                    continue;
+                }
+                $id = (int)($a['id'] ?? 0);
+                if ($id <= 0) {
+                    continue;
+                }
+                $nick = trim((string)($a['nickname'] ?? ''));
+                if ($nick === '') {
+                    $nick = trim((string)($a['username'] ?? ''));
+                }
+                $out[$id] = ['nickname' => $nick, 'group_name' => '', 'username' => trim((string)($a['username'] ?? ''))];
+            }
+        }
+        try {
+            $rows = Db::name('auth_group_access')
+                ->alias('aga')
+                ->join('auth_group ag', 'aga.group_id = ag.id', 'LEFT')
+                ->where('aga.uid', 'in', $adminIds)
+                ->field('aga.uid,ag.name')
+                ->select();
+        } catch (\Throwable $e) {
+            $rows = [];
+        }
+        if (is_array($rows)) {
+            foreach ($rows as $r) {
+                if (!is_array($r)) {
+                    continue;
+                }
+                $uid = (int)($r['uid'] ?? 0);
+                $gname = trim((string)($r['name'] ?? ''));
+                if ($uid <= 0 || $gname === '') {
+                    continue;
+                }
+                if (!isset($out[$uid])) {
+                    $out[$uid] = ['nickname' => '', 'group_name' => $gname];
+                } elseif ($out[$uid]['group_name'] === '') {
+                    $out[$uid]['group_name'] = $gname;
+                } elseif (strpos($out[$uid]['group_name'], $gname) === false) {
+                    $out[$uid]['group_name'] .= '、' . $gname;
+                }
+            }
+        }
+
+        return $out;
+    }
+
+    /**
+     * 多工序短时间内相同操作合并为一条,并带上工序名称
+     * 例:审核确认供应商:配套、快递已选定「xxx」
+     *
+     * @param array<int, array<string, mixed>> $logs
+     * @param array<int, string> $processNameBySid
+     * @return array<int, array<string, mixed>>
+     */
+    public static function mergeDuplicateProcessLogs(array $logs, array $processNameBySid = []): array
+    {
+        if ($logs === []) {
+            return [];
+        }
+        $groups = [];
+        $order = [];
+        foreach ($logs as $idx => $lg) {
+            if (!is_array($lg)) {
+                continue;
+            }
+            $adminId = (int)($lg[self::COL_ADMIN_ID] ?? 0);
+            $admin = trim((string)($lg[self::COL_ADMIN_NAME] ?? ''));
+            $action = trim((string)($lg['action_text'] ?? $lg[self::COL_ACTION] ?? ''));
+            $content = trim((string)($lg[self::COL_CONTENT] ?? ''));
+            $contentCore = self::normalizeMergeContentCore($action, $content);
+            $ts = self::logTimeTs($lg);
+            // 60 秒内、同操作人+同操作类型+同说明核心 → 合并
+            $bucket = null;
+            foreach ($groups as $gk => $g) {
+                if (($g['admin_id'] !== $adminId && $g['admin'] !== $admin)
+                    || $g['action'] !== $action
+                    || $g['content_core'] !== $contentCore) {
+                    continue;
+                }
+                if (abs($ts - (int)$g['ts']) <= 60) {
+                    $bucket = $gk;
+                    break;
+                }
+            }
+            if ($bucket === null) {
+                $bucket = 'g' . $idx;
+                $groups[$bucket] = [
+                    'admin_id'     => $adminId,
+                    'admin'        => $admin,
+                    'action'       => $action,
+                    'content_core' => $contentCore,
+                    'ts'           => $ts,
+                    'row'          => $lg,
+                    'sids'         => [],
+                    'contents'     => [],
+                ];
+                $order[] = $bucket;
+            }
+            $sid = (int)($lg[self::COL_SCYDGY_ID] ?? 0);
+            if ($sid !== 0) {
+                $groups[$bucket]['sids'][$sid] = true;
+            }
+            $groups[$bucket]['contents'][] = $content;
+            // 保留最早一条作展示底稿,时间用最早
+            $rowTs = self::logTimeTs($groups[$bucket]['row']);
+            if ($ts > 0 && ($rowTs === 0 || $ts < $rowTs)) {
+                $groups[$bucket]['row'] = $lg;
+                $groups[$bucket]['ts'] = $ts;
+            }
+        }
+
+        $out = [];
+        foreach ($order as $gk) {
+            $g = $groups[$gk];
+            $row = $g['row'];
+            $sids = array_keys($g['sids']);
+            $processNames = [];
+            foreach ($sids as $sid) {
+                $nm = trim((string)($processNameBySid[$sid] ?? ''));
+                if ($nm !== '' && !in_array($nm, $processNames, true)) {
+                    $processNames[] = $nm;
+                }
+            }
+            $content = trim((string)($row[self::COL_CONTENT] ?? ''));
+            if (count($sids) > 1 || count($processNames) > 1) {
+                $content = self::buildMergedProcessContent(
+                    (string)($g['action'] ?? ''),
+                    $content,
+                    $processNames
+                );
+            }
+            $row[self::COL_CONTENT] = $content;
+            $row['action_text'] = (string)($g['action'] !== '' ? $g['action'] : ($row['action_text'] ?? ''));
+            $out[] = $row;
+        }
+
+        return $out;
+    }
+
+    /**
+     * 提取可合并的内容核心(去掉前缀动作名后的业务说明)
+     */
+    protected static function normalizeMergeContentCore(string $action, string $content): string
+    {
+        return self::stripLeadingActionPrefixes(trim($content), trim($action));
+    }
+
+    /**
+     * @param array<int, string> $processNames
+     */
+    protected static function buildMergedProcessContent(string $action, string $content, array $processNames): string
+    {
+        $core = self::normalizeMergeContentCore($action, $content);
+        $procText = implode('、', $processNames);
+        if ($procText === '') {
+            return $core !== '' ? $core : $content;
+        }
+        // 工序名放在操作说明里,不盖住操作类型
+        if ($core !== '') {
+            return $procText . ' ' . $core;
+        }
+
+        return $procText;
+    }
+
+    /**
+     * @param array<string, mixed> $lg
+     */
+    protected static function logTimeTs(array $lg): int
+    {
+        $raw = trim((string)($lg['createtime_text'] ?? $lg[self::COL_TIME] ?? ''));
+        if ($raw === '') {
+            return 0;
+        }
+        $ts = strtotime(str_replace('T', ' ', $raw));
+
+        return ($ts !== false && $ts > 0) ? (int)$ts : 0;
+    }
+
     private static function cut(string $s, int $max): string
     {
         if (function_exists('mb_substr')) {

+ 151 - 19
application/common/library/ProcuremenSupplierScore.php

@@ -34,6 +34,15 @@ class ProcuremenSupplierScore
         if (self::$schemaReady === true) {
             return;
         }
+        // 跨请求缓存:避免每次打开详情都 SHOW COLUMNS / ALTER 探测
+        try {
+            if (\think\Cache::get('procuremen_supplier_score_schema_ok_v1')) {
+                self::$schemaReady = true;
+
+                return;
+            }
+        } catch (\Throwable $e) {
+        }
         try {
             Db::query('SELECT 1 FROM `' . self::TABLE_RULE . '` LIMIT 1');
             Db::query('SELECT 1 FROM `' . self::TABLE_SCORE . '` LIMIT 1');
@@ -43,6 +52,10 @@ class ProcuremenSupplierScore
             self::ensureLeadColumns();
             self::ensureIntegerColumns();
             self::ensureFinalScoreTable();
+            try {
+                \think\Cache::set('procuremen_supplier_score_schema_ok_v1', 1, 86400);
+            } catch (\Throwable $e) {
+            }
 
             return;
         } catch (\Throwable $e) {
@@ -69,6 +82,10 @@ class ProcuremenSupplierScore
         self::ensureLeadColumns();
         self::ensureIntegerColumns();
         self::ensureFinalScoreTable();
+        try {
+            \think\Cache::set('procuremen_supplier_score_schema_ok_v1', 1, 86400);
+        } catch (\Throwable $e) {
+        }
     }
 
     /** 月度记录表(商务技术分 / 价格分 / 最终得分,仅本页展示与导出) */
@@ -1087,6 +1104,17 @@ class ProcuremenSupplierScore
             }
         }
         foreach ($items as $cn => &$it) {
+            // 单价未填写:价格得分与总分均为 0
+            if ($pw > 0 && (empty($it['has_price']) || (float)($it['price_sum'] ?? 0) <= 0)) {
+                $it['price_score'] = 0.0;
+                $it['price_score_text'] = '0';
+                $it['lead_score'] = 0.0;
+                $it['lead_score_text'] = self::formatScore(0);
+                $it['score'] = 0.0;
+                $it['score_text'] = '0';
+                continue;
+            }
+
             // 价格得分 = (最低单价合计 / 本供应商单价合计) × 价格权重% × 100
             if ($pw > 0 && !empty($it['has_price']) && $it['price_sum'] > 0 && $minSum !== null && $minSum > 0) {
                 $ratio = round($minSum / (float)$it['price_sum'], 4);
@@ -1370,11 +1398,15 @@ class ProcuremenSupplierScore
         if ($ccydh === '') {
             return [];
         }
-        self::ensureSchema();
         try {
             $rows = Db::table(self::TABLE_SCORE)->where('ccydh', $ccydh)->select();
         } catch (\Throwable $e) {
-            return [];
+            try {
+                self::ensureSchema();
+                $rows = Db::table(self::TABLE_SCORE)->where('ccydh', $ccydh)->select();
+            } catch (\Throwable $e2) {
+                return [];
+            }
         }
         if (!is_array($rows)) {
             return [];
@@ -1388,11 +1420,8 @@ class ProcuremenSupplierScore
             if ($cn === '') {
                 continue;
             }
-            $score = (float)($r['score'] ?? 0);
-            $rank = (int)($r['rank_no'] ?? 0);
             $qualityScore = (int)round((float)($r['quality_score'] ?? 0));
             $priceSum = round((float)($r['price_sum'] ?? 0), 4);
-            $priceScore = (float)($r['price_score'] ?? 0);
             $qw = (int)($r['quality_weight'] ?? 50);
             $pw = (int)($r['price_weight'] ?? 50);
             $lw = (int)($r['lead_weight'] ?? 0);
@@ -1408,21 +1437,109 @@ class ProcuremenSupplierScore
                 'price_sum'            => $priceSum,
                 'price_sum_text'       => self::formatScore($priceSum),
                 'price_weight'         => $pw,
-                'price_score'          => $priceScore,
-                'price_score_text'     => self::formatScore($priceScore),
+                'price_score'          => (float)($r['price_score'] ?? 0),
+                'price_score_text'     => self::formatScore($r['price_score'] ?? 0),
                 'lead_days_sum'        => $leadSum,
                 'lead_days_sum_text'   => $leadEnabled ? (string)$leadSum : '',
                 'lead_weight'          => $lw,
                 'lead_enabled'         => $leadEnabled,
                 'lead_score'           => $leadScore,
                 'lead_score_text'      => $leadEnabled ? self::formatScore($leadScore) : '',
-                'score'                => $score,
-                'rank_no'              => $rank,
-                'score_text'           => self::formatScore($score),
-                'rank_text'            => $rank > 0 ? (string)$rank : '',
+                'score'                => (float)($r['score'] ?? 0),
+                'rank_no'              => (int)($r['rank_no'] ?? 0),
+                'score_text'           => self::formatScore($r['score'] ?? 0),
+                'rank_text'            => ((int)($r['rank_no'] ?? 0)) > 0 ? (string)(int)($r['rank_no'] ?? 0) : '',
+                '_row_id'              => (int)($r['id'] ?? 0),
             ];
         }
 
+        // 按单价合计重算价格得分/总分(兼容历史把权重算进价格得分的数据)
+        $minSum = null;
+        foreach ($out as $it) {
+            $ps = (float)($it['price_sum'] ?? 0);
+            if ($ps > 0 && ($minSum === null || $ps < $minSum)) {
+                $minSum = $ps;
+            }
+        }
+        foreach ($out as $cn => &$it) {
+            $qw = (int)($it['quality_weight'] ?? 50);
+            $pw = (int)($it['price_weight'] ?? 50);
+            $lw = (int)($it['lead_weight'] ?? 0);
+            $priceSum = (float)($it['price_sum'] ?? 0);
+            // 单价未填写:总分为 0
+            if ($pw > 0 && $priceSum <= 0) {
+                $it['price_score'] = 0.0;
+                $it['price_score_text'] = '0';
+                $it['score'] = 0.0;
+                $it['score_text'] = '0';
+                continue;
+            }
+            if ($pw > 0 && $priceSum > 0 && $minSum !== null && $minSum > 0) {
+                $ratio = round($minSum / $priceSum, 4);
+                $it['price_score'] = round($ratio * ($pw / 100) * 100, 2);
+            } else {
+                $it['price_score'] = 0.0;
+            }
+            $it['price_score_text'] = self::formatScore($it['price_score']);
+            $total = 0.0;
+            if ($qw > 0) {
+                $total += (float)$it['quality_score'] * ($qw / 100);
+            }
+            if ($pw > 0) {
+                $total += (float)$it['price_score'];
+            }
+            if ($lw > 0) {
+                $total += (float)$it['lead_score'] * ($lw / 100);
+            }
+            $it['score'] = round($total, 2);
+            $it['score_text'] = self::formatScore($it['score']);
+        }
+        unset($it);
+
+        $sorted = $out;
+        uasort($sorted, static function ($a, $b) {
+            $sa = (float)($a['score'] ?? 0);
+            $sb = (float)($b['score'] ?? 0);
+            if ($sa !== $sb) {
+                return $sb <=> $sa;
+            }
+
+            return strcmp((string)($a['company_name'] ?? ''), (string)($b['company_name'] ?? ''));
+        });
+        $rank = 0;
+        foreach ($sorted as $cn => $row) {
+            $rank++;
+            $oldRank = (int)($out[$cn]['rank_no'] ?? 0);
+            $out[$cn]['rank_no'] = $rank;
+            $out[$cn]['rank_text'] = (string)$rank;
+            $rowId = (int)($out[$cn]['_row_id'] ?? 0);
+            if ($rowId > 0) {
+                $upd = [];
+                if ($oldRank !== $rank) {
+                    $upd['rank_no'] = $rank;
+                }
+                foreach ($rows as $r) {
+                    if (!is_array($r) || (int)($r['id'] ?? 0) !== $rowId) {
+                        continue;
+                    }
+                    if (abs((float)($r['price_score'] ?? 0) - (float)$out[$cn]['price_score']) > 0.001) {
+                        $upd['price_score'] = $out[$cn]['price_score'];
+                    }
+                    if (abs((float)($r['score'] ?? 0) - (float)$out[$cn]['score']) > 0.001) {
+                        $upd['score'] = $out[$cn]['score'];
+                    }
+                    break;
+                }
+                if ($upd !== []) {
+                    try {
+                        Db::table(self::TABLE_SCORE)->where('id', $rowId)->update($upd);
+                    } catch (\Throwable $e) {
+                    }
+                }
+            }
+            unset($out[$cn]['_row_id']);
+        }
+
         return $out;
     }
 
@@ -1457,7 +1574,8 @@ class ProcuremenSupplierScore
     }
 
     /**
-     * 把排名/评分/计算留痕挂到报价组上(开标后可见)
+     * 把排名/评分挂到报价组上(开标后可见)
+     * 详情等读路径:优先读已落库评分,避免每次打开重算写库拖慢弹窗
      *
      * @param array<int, array<string, mixed>> $quoteGroups
      * @return array<int, array<string, mixed>>
@@ -1476,19 +1594,33 @@ class ProcuremenSupplierScore
             return $quoteGroups;
         }
 
-        // 每次按最新规则重算并落库,避免历史错误总分残留
-        $ym = date('Y-m');
-        $saved = self::calculateForQuoteGroups($quoteGroups, null, $ym);
-        try {
-            self::saveForOrder($ccydh, $quoteGroups, $ym);
-        } catch (\Throwable $e) {
+        $saved = self::loadSavedByCcydh($ccydh);
+        $needCalc = ($saved === []);
+        if (!$needCalc) {
+            $anyHit = false;
+            foreach ($quoteGroups as $g) {
+                if (!is_array($g)) {
+                    continue;
+                }
+                $cn = trim((string)($g['name'] ?? $g['company_name'] ?? ''));
+                if ($cn !== '' && isset($saved[$cn])) {
+                    $anyHit = true;
+                    break;
+                }
+            }
+            $needCalc = !$anyHit;
         }
+        // 无落库记录时仅内存计算用于展示;落库由开标验证 saveForOrder 负责
+        if ($needCalc) {
+            $saved = self::calculateForQuoteGroups($quoteGroups, null, date('Y-m'));
+        }
+
         foreach ($quoteGroups as &$g) {
             if (!is_array($g)) {
                 continue;
             }
             $cn = trim((string)($g['name'] ?? $g['company_name'] ?? ''));
-            $hit = $saved[$cn] ?? null;
+            $hit = ($cn !== '' && isset($saved[$cn]) && is_array($saved[$cn])) ? $saved[$cn] : null;
             if (is_array($hit)) {
                 $g['service_rank'] = (int)($hit['rank_no'] ?? 0);
                 $g['service_score'] = (float)($hit['score'] ?? 0);

+ 26 - 13
application/common/library/ProcuremenTime.php

@@ -169,39 +169,36 @@ class ProcuremenTime
         if ($action === '确认供应商') {
             if (preg_match('/选定[::]\s*(.+)$/u', $content, $m)) {
                 $name = trim($m[1]);
-                if (preg_match('/^确认供应商/u', $content) && preg_match('/订单([^),]+)/u', $content, $om)) {
-                    return '确认供应商(订单' . trim($om[1]) . '):已选定「' . $name . '」';
-                }
 
                 return '确认供应商:已选定「' . $name . '」';
             }
         }
         if ($action === '开标验证') {
             if (preg_match('/开标验证/u', $content)) {
-                return $content;
+                return self::stripOperLogOrderNo($content);
             }
-            return '开标验证:' . $content;
+            return '开标验证:' . self::stripOperLogOrderNo($content);
         }
         if ($action === '补加供应商') {
             if (preg_match('/^补加供应商/u', $content)) {
-                return $content;
+                return self::stripOperLogOrderNo($content);
             }
 
-            return '补加供应商:' . $content;
+            return '补加供应商:' . self::stripOperLogOrderNo($content);
         }
         if ($action === '重发短信') {
             if (preg_match('/重新发送短信/u', $content)) {
-                return $content;
+                return self::stripOperLogOrderNo($content);
             }
 
-            return '确认页重新发送短信:' . $content;
+            return '确认页重新发送短信:' . self::stripOperLogOrderNo($content);
         }
         if ($action === '重发邮件') {
             if (preg_match('/重新发送邮件/u', $content)) {
-                return $content;
+                return self::stripOperLogOrderNo($content);
             }
 
-            return '确认页重新发送邮件:' . $content;
+            return '确认页重新发送邮件:' . self::stripOperLogOrderNo($content);
         }
         if ($action === '下发通知' || $action === '下发') {
             if (preg_match('/通知供应商[((](\d+)\s*家[))].*?[::]\s*(.+)$/u', $content, $m)) {
@@ -215,9 +212,25 @@ class ProcuremenTime
         $content = preg_replace('/^(合并)?(协助|外发)初选/u', '下发', $content);
         $content = preg_replace('/^采购终审驳回/u', '审核驳回', $content);
         $content = preg_replace('/退回外发初选/u', '退回协助初选', $content);
-        $content = preg_replace('/^重新下发(([^)]+)),/u', '重新下发($1):', $content);
         $content = preg_replace('/;+$/u', '', $content);
 
-        return trim($content);
+        return trim(self::stripOperLogOrderNo((string)$content));
+    }
+
+    /**
+     * 展示时去掉说明里的订单号括号,如(订单YW…)(202605483S)
+     * 保留「(N 道工序)」等非订单号括号
+     */
+    protected static function stripOperLogOrderNo(string $content): string
+    {
+        $content = preg_replace('/(订单[^)]*)/u', '', $content);
+        $content = preg_replace('/\(订单[^)]*\)/u', '', $content);
+        // 重新下发(订单号)— 不含「工序」字样才去掉
+        $content = preg_replace('/^(重新下发|审批重新下发)((?![^)]*工序)[^)]+)/u', '$1', $content);
+        $content = preg_replace('/^(重新下发|审批重新下发)\((?![^)]*工序)[^)]+\)/u', '$1', $content);
+        $content = preg_replace('/:{2,}/u', ':', $content);
+        $content = preg_replace('/\s{2,}/u', ' ', $content);
+
+        return trim((string)$content);
     }
 }

+ 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.83',
+  'version' => '1.10.86',
   'timezone' => 'Asia/Shanghai',
   'forbiddenip' => '',
   'languages' => 

+ 85 - 3
application/index/controller/Index.php

@@ -762,6 +762,81 @@ class Index extends Frontend
         return md5(md5($password));
     }
 
+    /**
+     * 手机端强密码校验:通过返回空字符串,否则返回错误提示
+     */
+    protected function mprocValidateStrongPassword(string $password): string
+    {
+        $len = strlen($password);
+        if ($len < 8 || $len > 20) {
+            return '密码长度须为8~20位';
+        }
+        if (preg_match('/\s/', $password)) {
+            return '密码不能包含空格';
+        }
+        if (preg_match('/^\d+$/', $password)) {
+            return '密码不能为纯数字,请同时包含字母';
+        }
+        if (preg_match('/^[a-zA-Z]+$/', $password)) {
+            return '密码不能为纯字母,请同时包含数字';
+        }
+        if (!preg_match('/[a-zA-Z]/', $password) || !preg_match('/\d/', $password)) {
+            return '密码须同时包含字母和数字';
+        }
+        if (preg_match('/(.)\1{3,}/', $password)) {
+            return '密码不能包含过多重复字符(如1111、aaaa)';
+        }
+        if ($this->mprocPasswordHasSequentialRun($password, 4)) {
+            return '密码不能包含连续字符(如1234、abcd)';
+        }
+        $weak = [
+            '12345678', '123456789', '1234567890', '87654321', '01234567',
+            'password', 'password1', 'passw0rd', 'qwerty12', 'qwertyui',
+            'abc12345', 'abcd1234', 'a1b2c3d4', '11111111', '00000000',
+            '88888888', '66666666', '11223344', '1qaz2wsx', 'qazwsxed',
+        ];
+        if (in_array(strtolower($password), $weak, true)) {
+            return '密码过于简单,请重新设置';
+        }
+
+        return '';
+    }
+
+    /**
+     * 是否包含连续递增/递减片段(长度 >= $runLen),如 1234、4321、abcd
+     */
+    protected function mprocPasswordHasSequentialRun(string $password, int $runLen = 4): bool
+    {
+        $runLen = max(3, $runLen);
+        $lower = strtolower($password);
+        $n = strlen($lower);
+        if ($n < $runLen) {
+            return false;
+        }
+        for ($i = 0; $i <= $n - $runLen; $i++) {
+            $asc = true;
+            $desc = true;
+            for ($j = 1; $j < $runLen; $j++) {
+                $prev = ord($lower[$i + $j - 1]);
+                $cur = ord($lower[$i + $j]);
+                if ($cur !== $prev + 1) {
+                    $asc = false;
+                }
+                if ($cur !== $prev - 1) {
+                    $desc = false;
+                }
+                if (!$asc && !$desc) {
+                    break;
+                }
+            }
+            if ($asc || $desc) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
     /**
      * @param array<string, mixed> $cu
      * @return array<string, mixed>
@@ -3392,8 +3467,9 @@ class Index extends Frontend
         if ($oldPwd === '' || $newPwd === '' || $renewPwd === '') {
             $this->error('请填写完整');
         }
-        if (strlen($newPwd) < 4) {
-            $this->error('新密码至少4位');
+        $pwdErr = $this->mprocValidateStrongPassword($newPwd);
+        if ($pwdErr !== '') {
+            $this->error($pwdErr);
         }
         if ($newPwd !== $renewPwd) {
             $this->error('两次输入的新密码不一致');
@@ -3414,7 +3490,13 @@ class Index extends Frontend
         } catch (\Throwable $e) {
             $this->error('修改失败:' . $e->getMessage());
         }
-        $this->success('密码已修改');
+        // 改密后强制重新登录
+        $token = Session::get('mproc_token');
+        if ($token === null || $token === '') {
+            $token = Cookie::get('mproc_token');
+        }
+        $this->mprocClearLogin(preg_replace('/[^a-f0-9]/i', '', (string)$token));
+        $this->success('密码已修改,请重新登录', url('index/index/login'));
     }
 
     /**

+ 79 - 11
application/index/view/index/index.html

@@ -427,7 +427,8 @@
             font-size: 15px;
             line-height: 1.4;
             text-align: center;
-            white-space: nowrap;
+            white-space: normal;
+            word-break: break-word;
             pointer-events: none;
             opacity: 0;
             transition: opacity .2s ease;
@@ -832,15 +833,15 @@
         <p class="modal-head">修改密码</p>
         <div class="modal-field">
             <label for="inp-old-pwd">原密码</label>
-            <input type="password" id="inp-old-pwd" autocomplete="current-password" maxlength="64">
+            <input type="password" id="inp-old-pwd" autocomplete="current-password" maxlength="20">
         </div>
         <div class="modal-field">
             <label for="inp-new-pwd">新密码</label>
-            <input type="password" id="inp-new-pwd" autocomplete="new-password" maxlength="64">
+            <input type="password" id="inp-new-pwd" autocomplete="new-password" maxlength="20" placeholder="8~20位,须含字母和数字">
         </div>
         <div class="modal-field">
             <label for="inp-renew-pwd">确认新密码</label>
-            <input type="password" id="inp-renew-pwd" autocomplete="new-password" maxlength="64">
+            <input type="password" id="inp-renew-pwd" autocomplete="new-password" maxlength="20" placeholder="再次输入新密码">
         </div>
         <div class="modal-actions">
             <button type="button" class="btn-cancel" id="pwd-cancel">取消</button>
@@ -2747,15 +2748,20 @@
                 var newP = inpNewPwd ? inpNewPwd.value : '';
                 var renP = inpRenewPwd ? inpRenewPwd.value : '';
                 if (!oldP || !newP || !renP) {
-                    alert('请填写完整');
+                    mprocShowToast('请填写完整');
                     return;
                 }
-                if (newP.length < 4) {
-                    alert('新密码至少4位');
+                var pwdTip = mprocValidateStrongPassword(newP);
+                if (pwdTip) {
+                    mprocShowToast(pwdTip, 2200);
                     return;
                 }
                 if (newP !== renP) {
-                    alert('两次输入的新密码不一致');
+                    mprocShowToast('两次输入的新密码不一致');
+                    return;
+                }
+                if (oldP === newP) {
+                    mprocShowToast('新密码不能与旧密码相同');
                     return;
                 }
                 btnPwdSave.disabled = true;
@@ -2774,16 +2780,78 @@
                     btnPwdSave.disabled = false;
                     if (ret && (ret.code === 1 || ret.code === '1')) {
                         closePwdModal();
-                        alert(ret.msg || '密码已修改');
+                        mprocClearLoginToken();
+                        mprocShowToast(ret.msg || '密码已修改,请重新登录', 1600);
+                        var jump = (ret.url || ret.data && ret.data.url) || '';
+                        if (!jump) {
+                            jump = mprocEndpointUrl('login.html');
+                        }
+                        setTimeout(function () {
+                            window.location.href = jump;
+                        }, 900);
                     } else {
-                        alert(ret && ret.msg ? ret.msg : '修改失败');
+                        mprocShowToast(ret && ret.msg ? ret.msg : '修改失败', 2200);
                     }
                 }).catch(function () {
                     btnPwdSave.disabled = false;
-                    alert('网络错误');
+                    mprocShowToast('网络错误');
                 });
             });
         }
+
+        function mprocPasswordHasSequentialRun(pwd, runLen) {
+            runLen = runLen || 4;
+            var lower = String(pwd || '').toLowerCase();
+            var n = lower.length;
+            if (n < runLen) return false;
+            for (var i = 0; i <= n - runLen; i++) {
+                var asc = true, desc = true;
+                for (var j = 1; j < runLen; j++) {
+                    var prev = lower.charCodeAt(i + j - 1);
+                    var cur = lower.charCodeAt(i + j);
+                    if (cur !== prev + 1) asc = false;
+                    if (cur !== prev - 1) desc = false;
+                    if (!asc && !desc) break;
+                }
+                if (asc || desc) return true;
+            }
+            return false;
+        }
+
+        function mprocValidateStrongPassword(pwd) {
+            pwd = String(pwd || '');
+            if (pwd.length < 8 || pwd.length > 20) {
+                return '密码长度须为8~20位';
+            }
+            if (/\s/.test(pwd)) {
+                return '密码不能包含空格';
+            }
+            if (/^\d+$/.test(pwd)) {
+                return '密码不能为纯数字,请同时包含字母';
+            }
+            if (/^[a-zA-Z]+$/.test(pwd)) {
+                return '密码不能为纯字母,请同时包含数字';
+            }
+            if (!/[a-zA-Z]/.test(pwd) || !/\d/.test(pwd)) {
+                return '密码须同时包含字母和数字';
+            }
+            if (/(.)\1{3,}/.test(pwd)) {
+                return '密码不能包含过多重复字符(如1111、aaaa)';
+            }
+            if (mprocPasswordHasSequentialRun(pwd, 4)) {
+                return '密码不能包含连续字符(如1234、abcd)';
+            }
+            var weak = {
+                '12345678': 1, '123456789': 1, '1234567890': 1, '87654321': 1, '01234567': 1,
+                'password': 1, 'password1': 1, 'passw0rd': 1, 'qwerty12': 1, 'qwertyui': 1,
+                'abc12345': 1, 'abcd1234': 1, 'a1b2c3d4': 1, '11111111': 1, '00000000': 1,
+                '88888888': 1, '66666666': 1, '11223344': 1, '1qaz2wsx': 1, 'qazwsxed': 1
+            };
+            if (weak[pwd.toLowerCase()]) {
+                return '密码过于简单,请重新设置';
+            }
+            return '';
+        }
     }
 
     var logoutLink = document.getElementById('mproc-bar-logout');

+ 37 - 2
public/assets/js/backend/procuremen.js

@@ -1463,8 +1463,43 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                 {field: 'cGzzxMc', title: __('外厂单位'), operate: 'LIKE', table: 'a', width: 220, align: 'center', class: 'procuremen-cell-wrap'},
                 {field: 'MBZ', title: __('备注'), operate: 'LIKE', table: 'a', width: 150, align: 'center', class: 'procuremen-cell-wrap'},
                 {field: 'cywyxm', title: __('业务员'), operate: '=', searchList: procuremenSearchList('cywyxm'), table: 'b', width: 80, align: 'center'},
-                {field: 'dStamp', title: __('操作日期'), operate: 'RANGE', addclass: 'datetimerange', autocomplete: false, table: 'a', width: 176, align: 'center', sortable: true, class: 'procuremen-th-sort-tight'},
-                {field: 'dputrecord', title: __('提交日期'), operate: 'RANGE', addclass: 'datetimerange', autocomplete: false, table: 'b', width: 176, align: 'center', sortable: true, class: 'procuremen-th-sort-tight'},
+                {field: 'issue_time', title: '下发', operate: false, table: 'a', width: 160, align: 'center',
+                    visible: indexInitWffTab !== 'pick',
+                    formatter: function (v, row) {
+                        var t = (v != null && String(v).trim() !== '') ? String(v).trim()
+                            : (row && row.pick_time != null ? String(row.pick_time).trim() : '');
+                        if (!t || t.indexOf('0000-00-00') === 0) {
+                            return '—';
+                        }
+                        return procuremenEscHtml(t);
+                    }
+                },
+                {field: 'confirm_time', title: '确定', operate: false, table: 'a', width: 160, align: 'center',
+                    visible: indexInitWffTab !== 'pick',
+                    formatter: function (v) {
+                        var t = (v != null && String(v).trim() !== '') ? String(v).trim() : '';
+                        if (!t || t.indexOf('0000-00-00') === 0) {
+                            return '—';
+                        }
+                        return procuremenEscHtml(t);
+                    }
+                },
+                {field: 'approve_time', title: '审批', operate: false, table: 'a', width: 160, align: 'center',
+                    visible: indexInitWffTab !== 'pick',
+                    formatter: function (v) {
+                        var t = (v != null && String(v).trim() !== '') ? String(v).trim() : '';
+                        if (!t || t.indexOf('0000-00-00') === 0) {
+                            return '—';
+                        }
+                        return procuremenEscHtml(t);
+                    }
+                },
+                {field: 'dStamp', title: __('操作日期'), operate: 'RANGE', addclass: 'datetimerange', autocomplete: false, table: 'a', width: 176, align: 'center', sortable: true, class: 'procuremen-th-sort-tight',
+                    visible: indexInitWffTab === 'pick'
+                },
+                {field: 'dputrecord', title: __('提交日期'), operate: 'RANGE', addclass: 'datetimerange', autocomplete: false, table: 'b', width: 176, align: 'center', sortable: true, class: 'procuremen-th-sort-tight',
+                    visible: indexInitWffTab === 'pick'
+                },
                 {field: 'operate',title: '操作',width: 168,align: 'center',fixed: 'right', operate: false,
                     visible: indexInitWffTab !== 'pick',
                     table: table,

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

@@ -175,7 +175,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
 
             $(document).off('click.procuremenArchiveAbandon', '#btn-archive-abandon').on('click.procuremenArchiveAbandon', '#btn-archive-abandon', function (e) {
                 e.preventDefault();
-                if (!archiveCan('archiveabandon')) {
+                if (!archiveCan('auditabandon') && !archiveCan('archiveabandon')) {
                     Toastr.error('无重新下发权限');
                     return;
                 }
@@ -214,7 +214,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                 }, function (idx) {
                     Layer.close(idx);
                     Fast.api.ajax({
-                        url: 'procuremen/archiveabandon',
+                        url: 'procuremen/auditabandon',
                         type: 'POST',
                         data: {
                             scydgy_ids_json: JSON.stringify(idList),