m0_70156489 hace 5 días
padre
commit
a3dbaca724

+ 166 - 67
application/admin/controller/Supplierservicescore.php

@@ -43,12 +43,16 @@ class Supplierservicescore extends Backend
                 $limit = 50;
             }
 
-            $list = $this->loadMonthlySupplierScoreList($ym, $keyword);
+            // 有搜索词:跨月查该供应商;无搜索:按查询月份展示当月
+            $list = $keyword !== ''
+                ? $this->loadSupplierScoreSearchList($keyword)
+                : $this->loadMonthlySupplierScoreList($ym, '');
             $total = count($list);
             $pageRows = array_slice($list, $offset, $limit);
             $seqBase = $offset;
             $rows = [];
             foreach ($pageRows as $i => $item) {
+                $rowYm = (string)($item['ym'] ?? $ym);
                 $rows[] = [
                     'id'                   => $seqBase + $i + 1,
                     'seq_no'               => $seqBase + $i + 1,
@@ -63,7 +67,8 @@ class Supplierservicescore extends Backend
                     'final_score_text'     => (string)($item['final_score_text'] ?? ''),
                     'final_score_saved'    => !empty($item['final_score_saved']) ? 1 : 0,
                     'score_grade'          => (string)($item['score_grade'] ?? ''),
-                    'ym'                   => $ym,
+                    'score_date'           => (string)(($item['score_date'] ?? '') !== '' ? $item['score_date'] : $rowYm),
+                    'ym'                   => $rowYm,
                 ];
             }
 
@@ -103,10 +108,31 @@ class Supplierservicescore extends Backend
         if (!is_array($rows) || $rows === []) {
             $this->error('没有可保存的数据');
         }
-        $ret = ProcuremenSupplierScore::saveFinalScoresForYm($ym, $rows);
-        $done = (int)($ret['done'] ?? 0);
+        // 按行自带 ym 分组(跨月搜索结果可同页保存)
+        $byYm = [];
+        foreach ($rows as $row) {
+            if (!is_array($row)) {
+                continue;
+            }
+            $rowYm = trim((string)($row['ym'] ?? ''));
+            if (!preg_match('/^\d{4}-\d{2}$/', $rowYm)) {
+                $rowYm = $ym;
+            }
+            if (!isset($byYm[$rowYm])) {
+                $byYm[$rowYm] = [];
+            }
+            $byYm[$rowYm][] = $row;
+        }
+        $done = 0;
+        $err = '';
+        foreach ($byYm as $saveYm => $chunk) {
+            $ret = ProcuremenSupplierScore::saveFinalScoresForYm($saveYm, $chunk);
+            $done += (int)($ret['done'] ?? 0);
+            if ($err === '') {
+                $err = trim((string)($ret['error'] ?? ''));
+            }
+        }
         if ($done < 1) {
-            $err = trim((string)($ret['error'] ?? ''));
             $this->error($err !== '' ? $err : '保存失败,请检查填写的分数');
         }
         $this->success('操作成功');
@@ -114,7 +140,7 @@ class Supplierservicescore extends Backend
 
     /**
      * 导出供应商评审表
-     * 列:序号、供应商名称、商务/技术得分、价格得分、交货得分、最终得分、评分等级
+     * 列:序号、供应商、最终得分、评分等级
      */
     public function export()
     {
@@ -153,26 +179,23 @@ class Supplierservicescore extends Backend
             'wrapText'   => true,
         ];
 
-        $sheet->mergeCells('A1:G1');
+        $sheet->mergeCells('A1:D1');
         $sheet->setCellValue('A1', $title);
         $sheet->getRowDimension(1)->setRowHeight(36);
         $sheet->getStyle('A1')->getFont()->setName($fontName)->setBold(true)->setSize(20);
         $sheet->getStyle('A1')->getAlignment()->applyFromArray($centerAlign);
 
         $sheet->setCellValue('A2', '序号');
-        $sheet->setCellValue('B2', '供应商名称');
-        $sheet->setCellValue('C2', '商务/技术得分');
-        $sheet->setCellValue('D2', '价格得分');
-        $sheet->setCellValue('E2', '交货得分');
-        $sheet->setCellValue('F2', '最终得分');
-        $sheet->setCellValue('G2', '评分等级');
+        $sheet->setCellValue('B2', '供应商');
+        $sheet->setCellValue('C2', '最终得分');
+        $sheet->setCellValue('D2', '评分等级');
         $sheet->getRowDimension(2)->setRowHeight(26);
-        $sheet->getStyle('A2:G2')->getFont()->setName($fontName)->setBold(true)->setSize(14);
-        $sheet->getStyle('A2:G2')->getAlignment()->applyFromArray($centerAlign);
+        $sheet->getStyle('A2:D2')->getFont()->setName($fontName)->setBold(true)->setSize(14);
+        $sheet->getStyle('A2:D2')->getAlignment()->applyFromArray($centerAlign);
 
         $rowNum = 3;
         if ($list === []) {
-            $sheet->mergeCells('A3:G3');
+            $sheet->mergeCells('A3:D3');
             $sheet->setCellValue('A3', '(该月暂无供应商评审记录)');
             $sheet->getRowDimension(3)->setRowHeight(26);
             $sheet->getStyle('A3')->getFont()->setName($fontName)->setSize(12);
@@ -181,14 +204,10 @@ class Supplierservicescore extends Backend
         } else {
             $i = 1;
             foreach ($list as $item) {
-                $finalText = (string)($item['final_score_text'] ?? '');
                 $sheet->setCellValue('A' . $rowNum, $i);
                 $sheet->setCellValue('B' . $rowNum, (string)($item['company_name'] ?? ''));
-                $sheet->setCellValue('C' . $rowNum, (string)($item['quality_score_text'] ?? ''));
-                $sheet->setCellValue('D' . $rowNum, (string)($item['price_score_text'] ?? ''));
-                $sheet->setCellValue('E' . $rowNum, (string)($item['delivery_score_text'] ?? ''));
-                $sheet->setCellValue('F' . $rowNum, $finalText);
-                $sheet->setCellValue('G' . $rowNum, (string)($item['score_grade'] ?? ''));
+                $sheet->setCellValue('C' . $rowNum, (string)($item['final_score_text'] ?? ''));
+                $sheet->setCellValue('D' . $rowNum, (string)($item['score_grade'] ?? ''));
                 $sheet->getRowDimension($rowNum)->setRowHeight(24);
                 $rowNum++;
                 $i++;
@@ -196,16 +215,13 @@ class Supplierservicescore extends Backend
         }
 
         $lastRow = max(2, $rowNum - 1);
-        $sheet->getStyle('A1:G' . $lastRow)->applyFromArray($borderStyle);
-        $sheet->getStyle('A3:G' . $lastRow)->getFont()->setName($fontName)->setSize(12);
-        $sheet->getStyle('A3:G' . $lastRow)->getAlignment()->applyFromArray($centerAlign);
+        $sheet->getStyle('A1:D' . $lastRow)->applyFromArray($borderStyle);
+        $sheet->getStyle('A3:D' . $lastRow)->getFont()->setName($fontName)->setSize(12);
+        $sheet->getStyle('A3:D' . $lastRow)->getAlignment()->applyFromArray($centerAlign);
         $sheet->getColumnDimension('A')->setWidth(12);
         $sheet->getColumnDimension('B')->setWidth(48);
         $sheet->getColumnDimension('C')->setWidth(14);
-        $sheet->getColumnDimension('D')->setWidth(14);
-        $sheet->getColumnDimension('E')->setWidth(12);
-        $sheet->getColumnDimension('F')->setWidth(14);
-        $sheet->getColumnDimension('G')->setWidth(12);
+        $sheet->getColumnDimension('D')->setWidth(12);
         $sheet->getPageSetup()->setFitToPage(true)->setFitToWidth(1)->setFitToHeight(0);
 
         $filename = $title . '.xlsx';
@@ -239,52 +255,135 @@ class Supplierservicescore extends Backend
             if ($keyword !== '' && mb_stripos($name, $keyword) === false) {
                 continue;
             }
-            $quality = array_key_exists('quality_score', $item) ? $item['quality_score'] : null;
-            $price = array_key_exists('price_score', $item) ? $item['price_score'] : null;
-            $delivery = array_key_exists('delivery_score', $item) ? $item['delivery_score'] : null;
-            if ($quality !== null) {
-                $quality = (float)$quality;
-            }
-            if ($price !== null) {
-                $price = (float)$price;
+            $list[] = $this->buildSupplierScoreListItem($name, $item, $ym);
+        }
+        usort($list, [$this, 'sortSupplierScoreListItems']);
+
+        return $list;
+    }
+
+    /**
+     * 按供应商关键词跨月搜索(不限查询月份)
+     *
+     * @return array<int, array<string, mixed>>
+     */
+    protected function loadSupplierScoreSearchList(string $keyword): array
+    {
+        $keyword = trim($keyword);
+        if ($keyword === '') {
+            return [];
+        }
+        ProcuremenSupplierScore::ensureSchema();
+        ProcuremenSupplierScore::ensureFinalScoreTable();
+        try {
+            $rows = \think\Db::table(ProcuremenSupplierScore::TABLE_FINAL)
+                ->where('company_name', 'like', '%' . addcslashes($keyword, '%_\\') . '%')
+                ->field('ym,company_name,score,quality_score,price_score,delivery_score,final_score,score_grade')
+                ->order('ym', 'desc')
+                ->order('company_name', 'asc')
+                ->select();
+        } catch (\Throwable $e) {
+            return [];
+        }
+        if (!is_array($rows)) {
+            return [];
+        }
+        $list = [];
+        foreach ($rows as $r) {
+            if (!is_array($r)) {
+                continue;
             }
-            if ($delivery !== null) {
-                $delivery = (float)$delivery;
+            $name = trim((string)($r['company_name'] ?? ''));
+            $rowYm = ProcuremenSupplierScore::formatScoreYm($r['ym'] ?? '');
+            if ($name === '' || $rowYm === '') {
+                continue;
             }
-            $hasFinal = !empty($item['final_saved']);
-            $final = $hasFinal ? (float)($item['final_score'] ?? 0) : null;
-            // 列表排序:有最终得分按最终得分,否则按已填分项合计
-            $sortScore = $hasFinal
-                ? (float)$final
-                : (($quality ?? 0) + ($price ?? 0) + ($delivery ?? 0));
-            $list[] = [
-                'company_name'         => $name,
-                'quality_score'        => $quality,
-                'quality_score_text'   => ProcuremenSupplierScore::formatOptionalScore($quality),
-                'price_score'          => $price,
-                'price_score_text'     => ProcuremenSupplierScore::formatOptionalScore($price),
-                'delivery_score'       => $delivery,
-                'delivery_score_text'  => ProcuremenSupplierScore::formatOptionalScore($delivery),
-                'final_score'          => $final,
-                'final_score_text'     => $hasFinal ? ProcuremenSupplierScore::formatOptionalScore($final) : '',
-                'final_score_saved'    => $hasFinal ? 1 : 0,
-                'score_grade'          => ProcuremenSupplierScore::normalizeScoreGrade($item['score_grade'] ?? ''),
-                'sort_score'           => $sortScore,
+            $quality = ProcuremenSupplierScore::readOptionalScoreValue($r['quality_score'] ?? null);
+            $price = ProcuremenSupplierScore::readOptionalScoreValue($r['price_score'] ?? null);
+            $delivery = ProcuremenSupplierScore::readOptionalScoreValue($r['delivery_score'] ?? null);
+            $final = ProcuremenSupplierScore::readOptionalScoreValue($r['final_score'] ?? null);
+            $item = [
+                'quality_score'  => $quality,
+                'price_score'    => $price,
+                'delivery_score' => $delivery,
+                'final_score'    => $final,
+                'final_saved'    => $final !== null ? 1 : 0,
+                'score_grade'    => ProcuremenSupplierScore::normalizeScoreGrade($r['score_grade'] ?? ''),
+                'score_date'     => $rowYm,
             ];
+            $list[] = $this->buildSupplierScoreListItem($name, $item, $rowYm);
         }
-        usort($list, function ($a, $b) {
-            $sa = (float)($a['sort_score'] ?? 0);
-            $sb = (float)($b['sort_score'] ?? 0);
-            if ($sa !== $sb) {
-                return $sb <=> $sa;
-            }
-
-            return strcmp((string)($a['company_name'] ?? ''), (string)($b['company_name'] ?? ''));
-        });
+        usort($list, [$this, 'sortSupplierScoreListItems']);
 
         return $list;
     }
 
+    /**
+     * @param array<string, mixed> $item
+     * @return array<string, mixed>
+     */
+    protected function buildSupplierScoreListItem(string $name, array $item, string $ym): array
+    {
+        $quality = array_key_exists('quality_score', $item) ? $item['quality_score'] : null;
+        $price = array_key_exists('price_score', $item) ? $item['price_score'] : null;
+        $delivery = array_key_exists('delivery_score', $item) ? $item['delivery_score'] : null;
+        if ($quality !== null) {
+            $quality = (float)$quality;
+        }
+        if ($price !== null) {
+            $price = (float)$price;
+        }
+        if ($delivery !== null) {
+            $delivery = (float)$delivery;
+        }
+        $hasFinal = !empty($item['final_saved']);
+        $final = $hasFinal ? (float)($item['final_score'] ?? 0) : null;
+        $sortScore = $hasFinal
+            ? (float)$final
+            : (($quality ?? 0) + ($price ?? 0) + ($delivery ?? 0));
+        $scoreDate = (string)($item['score_date'] ?? '');
+        if ($scoreDate === '') {
+            $scoreDate = ProcuremenSupplierScore::formatScoreYm($ym);
+        }
+
+        return [
+            'company_name'         => $name,
+            'ym'                   => ProcuremenSupplierScore::formatScoreYm($ym),
+            'quality_score'        => $quality,
+            'quality_score_text'   => ProcuremenSupplierScore::formatOptionalScore($quality),
+            'price_score'          => $price,
+            'price_score_text'     => ProcuremenSupplierScore::formatOptionalScore($price),
+            'delivery_score'       => $delivery,
+            'delivery_score_text'  => ProcuremenSupplierScore::formatOptionalScore($delivery),
+            'final_score'          => $final,
+            'final_score_text'     => $hasFinal ? ProcuremenSupplierScore::formatOptionalScore($final) : '',
+            'final_score_saved'    => $hasFinal ? 1 : 0,
+            'score_grade'          => ProcuremenSupplierScore::normalizeScoreGrade($item['score_grade'] ?? ''),
+            'score_date'           => $scoreDate,
+            'sort_score'           => $sortScore,
+        ];
+    }
+
+    /**
+     * @param array<string, mixed> $a
+     * @param array<string, mixed> $b
+     */
+    protected function sortSupplierScoreListItems(array $a, array $b): int
+    {
+        $ya = (string)($a['ym'] ?? '');
+        $yb = (string)($b['ym'] ?? '');
+        if ($ya !== $yb) {
+            return strcmp($yb, $ya);
+        }
+        $sa = (float)($a['sort_score'] ?? 0);
+        $sb = (float)($b['sort_score'] ?? 0);
+        if ($sa !== $sb) {
+            return $sb <=> $sa;
+        }
+
+        return strcmp((string)($a['company_name'] ?? ''), (string)($b['company_name'] ?? ''));
+    }
+
     protected function resolveRequestYm(): string
     {
         $ym = trim((string)$this->request->get('ym', ''));

+ 22 - 4
application/admin/view/supplierservicescore/index.html

@@ -1,5 +1,5 @@
 <style>
-    /* 表格铺满内容区,表头与单元格居中 */
+    /* 表格铺满内容区,表头与单元格强制居中 */
     .supplierservicescore-page .bootstrap-table > .fixed-table-container,
     .supplierservicescore-page .bootstrap-table > .fixed-table-pagination {
         max-width: none;
@@ -12,14 +12,32 @@
         max-width: none;
         table-layout: fixed;
     }
-    .supplierservicescore-page .bootstrap-table .table td,
-    .supplierservicescore-page .bootstrap-table .table th {
+    .supplierservicescore-page .bootstrap-table .table > thead > tr > th,
+    .supplierservicescore-page .bootstrap-table .table > tbody > tr > td,
+    .supplierservicescore-page .bootstrap-table .fixed-table-header .table > thead > tr > th,
+    .supplierservicescore-page .bootstrap-table .fixed-table-body .table > tbody > tr > td,
+    .supplierservicescore-page .bootstrap-table .fixed-table-container thead th,
+    .supplierservicescore-page .bootstrap-table .fixed-table-container tbody td {
         white-space: nowrap;
         overflow: hidden;
         text-overflow: ellipsis;
         vertical-align: middle !important;
         text-align: center !important;
     }
+    .supplierservicescore-page .bootstrap-table .fixed-table-container thead th .th-inner,
+    .supplierservicescore-page .bootstrap-table .fixed-table-container thead th .th-inner.sortable,
+    .supplierservicescore-page .bootstrap-table .fixed-table-container thead th .sortable,
+    .supplierservicescore-page .bootstrap-table .table > thead > tr > th .th-inner,
+    .supplierservicescore-page .bootstrap-table .fixed-table-header .table > thead > tr > th .th-inner {
+        display: block !important;
+        width: 100% !important;
+        box-sizing: border-box !important;
+        text-align: center !important;
+        padding-left: 8px !important;
+        padding-right: 8px !important;
+        margin: 0 auto !important;
+        float: none !important;
+    }
     .supplierservicescore-page .bootstrap-table .table td .ssc-final-score-input,
     .supplierservicescore-page .bootstrap-table .table td .ssc-score-grade-select {
         overflow: visible;
@@ -72,7 +90,7 @@
                     <input type="month" id="export-review-ym" class="form-control input-sm" style="display:inline-block;width:150px;height:30px;vertical-align:middle;" title="选择月份后自动查询该月评分"/>
                 </span>
             </div>
-            <table id="table" class="table table-striped table-bordered table-hover table-nowrap"></table>
+            <table id="table" class="table table-striped table-bordered table-hover table-nowrap text-center"></table>
         </div>
     </div>
 </div>

+ 37 - 3
application/common/library/ProcuremenSupplierScore.php

@@ -448,7 +448,7 @@ class ProcuremenSupplierScore
     /**
      * 读取某月评审记录(仅读月度表;先按当月开标/询价供应商补齐名称)
      *
-     * @return array<string, array{score:float,quality_score:?float,price_score:?float,delivery_score:?float,final_score:?float,final_saved:int,score_grade:string}>
+     * @return array<string, array{score:float,quality_score:?float,price_score:?float,delivery_score:?float,final_score:?float,final_saved:int,score_grade:string,score_date:string}>
      */
     public static function loadMonthlyMapByYm(string $ym): array
     {
@@ -462,7 +462,7 @@ class ProcuremenSupplierScore
         try {
             $rows = Db::table(self::TABLE_FINAL)
                 ->where('ym', $ym)
-                ->field('company_name,score,quality_score,price_score,delivery_score,final_score,score_grade')
+                ->field('company_name,score,quality_score,price_score,delivery_score,final_score,score_grade,ym')
                 ->select();
         } catch (\Throwable $e) {
             return [];
@@ -475,7 +475,7 @@ class ProcuremenSupplierScore
             if (!is_array($r)) {
                 continue;
             }
-            $cn = trim((string)($r['company_name'] ?? ''));
+            $cn = trim((string)($r['company_name'] ?? $r['Company_name'] ?? ''));
             if ($cn === '') {
                 continue;
             }
@@ -491,12 +491,46 @@ class ProcuremenSupplierScore
                 'final_score'    => $final,
                 'final_saved'    => $final !== null ? 1 : 0,
                 'score_grade'    => self::normalizeScoreGrade($r['score_grade'] ?? ''),
+                // 列表「日期」列按 ym(YYYY-MM)展示,不到日
+                'score_date'     => self::formatScoreYm($r['ym'] ?? $ym),
             ];
         }
 
         return $out;
     }
 
+    /**
+     * 规范为 YYYY-MM(月度评审日期)
+     *
+     * @param mixed $raw
+     */
+    public static function formatScoreYm($raw): string
+    {
+        $raw = trim((string)$raw);
+        if (preg_match('/^\d{4}-\d{2}$/', $raw)) {
+            return $raw;
+        }
+        if (preg_match('/^(\d{4})\D+(\d{1,2})/', $raw, $m)) {
+            return sprintf('%04d-%02d', (int)$m[1], (int)$m[2]);
+        }
+
+        return '';
+    }
+
+    /**
+     * @param mixed $raw
+     */
+    public static function formatScoreDateYmd($raw): string
+    {
+        $raw = trim((string)$raw);
+        if ($raw === '' || stripos($raw, '0000-00-00') === 0) {
+            return '';
+        }
+        $ts = strtotime(str_replace('T', ' ', $raw));
+
+        return ($ts !== false && $ts > 0) ? date('Y-m-d', $ts) : '';
+    }
+
     /**
      * 解析可选分数:空=null;非法=false;合法=float
      *

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

+ 93 - 21
public/assets/js/backend/supplierservicescore.js

@@ -40,10 +40,11 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                     .replace(/</g, '&lt;')
                     .replace(/>/g, '&gt;');
             }
-            function scoreInputHtml(company, cssClass, val, title) {
+            function scoreInputHtml(company, ym, cssClass, val, title) {
                 var show = (val == null || val === '') ? '' : String(val);
                 return '<input type="text" inputmode="decimal" class="form-control input-sm ' + cssClass + '"'
                     + ' data-company="' + escAttr(company) + '"'
+                    + ' data-ym="' + escAttr(ym || '') + '"'
                     + ' value="' + escAttr(show) + '"'
                     + ' title="' + escAttr(title || '') + '"/>';
             }
@@ -58,13 +59,22 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                 }
                 return String(v);
             }
+            function rowYm(row) {
+                var v = String(row && row.ym ? row.ym : '').trim();
+                if (/^\d{4}-\d{2}$/.test(v)) {
+                    return v;
+                }
+                return resolveYm();
+            }
             function collectFinalRows() {
                 var rows = [];
-                var byCompany = {};
-                function ensure(company) {
-                    if (!byCompany[company]) {
-                        byCompany[company] = {
+                var byKey = {};
+                function ensure(company, ym) {
+                    var key = String(ym || '') + '|' + company;
+                    if (!byKey[key]) {
+                        byKey[key] = {
                             company_name: company,
+                            ym: ym || '',
                             quality_score: '',
                             price_score: '',
                             delivery_score: '',
@@ -72,44 +82,56 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                             score_grade: ''
                         };
                     }
-                    return byCompany[company];
+                    return byKey[key];
+                }
+                function pickYm($el) {
+                    var ym = String($el.data('ym') || '').trim();
+                    if (/^\d{4}-\d{2}$/.test(ym)) {
+                        return ym;
+                    }
+                    return resolveYm();
                 }
                 $('#table').find('input.ssc-quality-score-input').each(function () {
-                    var company = String($(this).data('company') || '').trim();
+                    var $el = $(this);
+                    var company = String($el.data('company') || '').trim();
                     if (!company) {
                         return;
                     }
-                    ensure(company).quality_score = String($(this).val() || '').trim();
+                    ensure(company, pickYm($el)).quality_score = String($el.val() || '').trim();
                 });
                 $('#table').find('input.ssc-price-score-input').each(function () {
-                    var company = String($(this).data('company') || '').trim();
+                    var $el = $(this);
+                    var company = String($el.data('company') || '').trim();
                     if (!company) {
                         return;
                     }
-                    ensure(company).price_score = String($(this).val() || '').trim();
+                    ensure(company, pickYm($el)).price_score = String($el.val() || '').trim();
                 });
                 $('#table').find('input.ssc-delivery-score-input').each(function () {
-                    var company = String($(this).data('company') || '').trim();
+                    var $el = $(this);
+                    var company = String($el.data('company') || '').trim();
                     if (!company) {
                         return;
                     }
-                    ensure(company).delivery_score = String($(this).val() || '').trim();
+                    ensure(company, pickYm($el)).delivery_score = String($el.val() || '').trim();
                 });
                 $('#table').find('input.ssc-final-score-input').each(function () {
-                    var company = String($(this).data('company') || '').trim();
+                    var $el = $(this);
+                    var company = String($el.data('company') || '').trim();
                     if (!company) {
                         return;
                     }
-                    ensure(company).final_score = String($(this).val() || '').trim();
+                    ensure(company, pickYm($el)).final_score = String($el.val() || '').trim();
                 });
                 $('#table').find('select.ssc-score-grade-select').each(function () {
-                    var company = String($(this).data('company') || '').trim();
+                    var $el = $(this);
+                    var company = String($el.data('company') || '').trim();
                     if (!company) {
                         return;
                     }
-                    ensure(company).score_grade = String($(this).val() || '').trim().toUpperCase();
+                    ensure(company, pickYm($el)).score_grade = String($el.val() || '').trim().toUpperCase();
                 });
-                $.each(byCompany, function (_, row) {
+                $.each(byKey, function (_, row) {
                     rows.push(row);
                 });
                 return rows;
@@ -120,6 +142,16 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                 $ymInput.val(currentYm());
             }
 
+            function forceTableCenterAlign() {
+                var $wrap = $('.supplierservicescore-page .bootstrap-table');
+                if (!$wrap.length) {
+                    $wrap = $('#table').closest('.bootstrap-table');
+                }
+                $wrap.find('thead th, tbody td, thead th .th-inner').each(function () {
+                    this.style.setProperty('text-align', 'center', 'important');
+                });
+            }
+
             var table = $('#table');
             table.bootstrapTable({
                 url: $.fn.bootstrapTable.defaults.extend.index_url,
@@ -132,6 +164,13 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                 searchPlaceholder: '搜索供应商',
                 pageSize: 100,
                 pageList: [50, 100, 200, 500],
+                classes: 'table table-striped table-bordered table-hover table-nowrap text-center',
+                onPostHeader: function () {
+                    forceTableCenterAlign();
+                },
+                onPostBody: function () {
+                    forceTableCenterAlign();
+                },
                 columns: [[
                     {
                         field: 'seq_no',
@@ -141,6 +180,8 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                         width: 60,
                         align: 'center',
                         halign: 'center',
+                        valign: 'middle',
+                        class: 'text-center',
                         formatter: function (value, row, index) {
                             if (value != null && value !== '') {
                                 return value;
@@ -157,6 +198,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                         operate: 'LIKE',
                         align: 'center',
                         halign: 'center',
+                        class: 'text-center',
                         width: 320
                     },
                     {
@@ -166,10 +208,11 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                         sortable: false,
                         align: 'center',
                         halign: 'center',
+                        class: 'text-center',
                         width: 120,
                         formatter: function (value, row) {
                             var company = row.company_name || '';
-                            return scoreInputHtml(company, 'ssc-quality-score-input', scoreCellVal(row, 'quality_score_text', 'quality_score'), '商务/技术得分(可改)');
+                            return scoreInputHtml(company, rowYm(row), 'ssc-quality-score-input', scoreCellVal(row, 'quality_score_text', 'quality_score'), '商务/技术得分(可改)');
                         }
                     },
                     {
@@ -179,10 +222,11 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                         sortable: false,
                         align: 'center',
                         halign: 'center',
+                        class: 'text-center',
                         width: 120,
                         formatter: function (value, row) {
                             var company = row.company_name || '';
-                            return scoreInputHtml(company, 'ssc-price-score-input', scoreCellVal(row, 'price_score_text', 'price_score'), '价格得分(可改)');
+                            return scoreInputHtml(company, rowYm(row), 'ssc-price-score-input', scoreCellVal(row, 'price_score_text', 'price_score'), '价格得分(可改)');
                         }
                     },
                     {
@@ -192,10 +236,11 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                         sortable: false,
                         align: 'center',
                         halign: 'center',
+                        class: 'text-center',
                         width: 120,
                         formatter: function (value, row) {
                             var company = row.company_name || '';
-                            return scoreInputHtml(company, 'ssc-delivery-score-input', scoreCellVal(row, 'delivery_score_text', 'delivery_score'), '交货得分(可改)');
+                            return scoreInputHtml(company, rowYm(row), 'ssc-delivery-score-input', scoreCellVal(row, 'delivery_score_text', 'delivery_score'), '交货得分(可改)');
                         }
                     },
                     {
@@ -205,6 +250,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                         sortable: false,
                         align: 'center',
                         halign: 'center',
+                        class: 'text-center',
                         width: 130,
                         formatter: function (value, row) {
                             var company = row.company_name || '';
@@ -212,7 +258,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                             if (parseInt(row.final_score_saved, 10) === 1) {
                                 val = scoreCellVal(row, 'final_score_text', 'final_score');
                             }
-                            return scoreInputHtml(company, 'ssc-final-score-input', val, '最终得分(可改)');
+                            return scoreInputHtml(company, rowYm(row), 'ssc-final-score-input', val, '最终得分(可改)');
                         }
                     },
                     {
@@ -222,9 +268,11 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                         sortable: false,
                         align: 'center',
                         halign: 'center',
+                        class: 'text-center',
                         width: 120,
                         formatter: function (value, row) {
                             var company = row.company_name || '';
+                            var ym = rowYm(row);
                             var cur = String(row.score_grade || '').trim().toUpperCase();
                             if (['A', 'B', 'C', 'D'].indexOf(cur) < 0) {
                                 cur = '';
@@ -238,6 +286,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                             ];
                             var html = '<select class="form-control input-sm ssc-score-grade-select"'
                                 + ' data-company="' + escAttr(company) + '"'
+                                + ' data-ym="' + escAttr(ym) + '"'
                                 + ' title="评分等级">';
                             opts.forEach(function (o) {
                                 html += '<option value="' + o.v + '"' + (cur === o.v ? ' selected' : '') + '>'
@@ -246,6 +295,29 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                             html += '</select>';
                             return html;
                         }
+                    },
+                    {
+                        field: 'score_date',
+                        title: '日期',
+                        operate: false,
+                        sortable: false,
+                        align: 'center',
+                        halign: 'center',
+                        valign: 'middle',
+                        class: 'text-center',
+                        width: 120,
+                        formatter: function (value, row) {
+                            var v = String(value == null ? '' : value).trim();
+                            if (!v) {
+                                v = String(row && row.ym ? row.ym : '').trim();
+                            }
+                            // ym 仅到月:统一显示 YYYY-MM
+                            var m = v.match(/^(\d{4})-(\d{1,2})/);
+                            if (m) {
+                                return m[1] + '-' + (m[2].length === 1 ? '0' + m[2] : m[2]);
+                            }
+                            return v || '—';
+                        }
                     }
                 ]],
                 queryParams: function (params) {