Преглед изворни кода

订单_供应_监测前端

Gogs пре 3 недеља
родитељ
комит
3f044b5286

+ 705 - 0
client-preview.html

@@ -0,0 +1,705 @@
+<!doctype html>
+<html lang="zh-CN">
+<head>
+  <meta charset="utf-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1">
+  <title>DTM 业务监测版块预览</title>
+  <style>
+    * {
+      box-sizing: border-box;
+    }
+
+    body {
+      margin: 0;
+      font-family: "Microsoft YaHei", "PingFang SC", Arial, sans-serif;
+      background: #eef3fb;
+      color: #17243b;
+    }
+
+    .preview-shell {
+      max-width: 1440px;
+      margin: 0 auto;
+      padding: 28px;
+    }
+
+    .topbar {
+      display: flex;
+      align-items: center;
+      justify-content: space-between;
+      gap: 20px;
+      margin-bottom: 22px;
+    }
+
+    .brand {
+      display: flex;
+      align-items: center;
+      gap: 12px;
+      font-weight: 800;
+      color: #101b33;
+      font-size: 18px;
+    }
+
+    .brand-mark {
+      width: 42px;
+      height: 42px;
+      border-radius: 14px;
+      background: linear-gradient(145deg, #7935f5 0%, #6028ee 100%);
+      color: #fff;
+      display: grid;
+      place-items: center;
+      box-shadow: 0 14px 28px rgba(99, 50, 236, 0.22);
+    }
+
+    .nav-pills {
+      display: flex;
+      gap: 8px;
+      padding: 4px;
+      border-radius: 16px;
+      background: #fff;
+      box-shadow: 0 14px 30px rgba(42, 61, 115, 0.08);
+    }
+
+    .nav-pills a {
+      padding: 10px 16px;
+      border-radius: 12px;
+      color: #6e778b;
+      font-size: 14px;
+      font-weight: 700;
+      text-decoration: none;
+    }
+
+    .nav-pills a.active {
+      background: #6267f2;
+      color: #fff;
+    }
+
+    .hero {
+      margin-bottom: 24px;
+      padding: 30px 34px;
+      border-radius: 26px;
+      background: linear-gradient(110deg, #ffffff 0%, #f7f9ff 56%, #eaf0ff 100%);
+      box-shadow: 0 20px 45px rgba(42, 61, 115, 0.08);
+      display: flex;
+      align-items: center;
+      justify-content: space-between;
+      gap: 24px;
+    }
+
+    .eyebrow {
+      margin: 0 0 8px;
+      color: #6267f2;
+      font-size: 12px;
+      font-weight: 800;
+      letter-spacing: .5px;
+      text-transform: uppercase;
+    }
+
+    h1,
+    h2,
+    h3,
+    p {
+      margin-top: 0;
+    }
+
+    .hero h1 {
+      margin-bottom: 10px;
+      color: #101b33;
+      font-size: 32px;
+      line-height: 1.2;
+    }
+
+    .hero p:last-child {
+      max-width: 700px;
+      margin-bottom: 0;
+      color: #7b8498;
+      line-height: 1.8;
+    }
+
+    .hero-visual {
+      min-width: 174px;
+      height: 112px;
+      border-radius: 24px;
+      background: #fff;
+      box-shadow: 0 18px 40px rgba(91, 105, 247, 0.18);
+      display: flex;
+      align-items: flex-end;
+      justify-content: center;
+      gap: 10px;
+      padding: 22px;
+    }
+
+    .bar {
+      width: 12px;
+      border-radius: 999px;
+      background: #ffad55;
+    }
+
+    .bar:nth-child(1) { height: 36px; }
+    .bar:nth-child(2) { height: 60px; background: #6267f2; }
+    .bar:nth-child(3) { height: 46px; }
+    .bar:nth-child(4) { height: 72px; background: #ef4c43; }
+
+    .section {
+      margin-bottom: 30px;
+    }
+
+    .section-header {
+      display: flex;
+      align-items: flex-end;
+      justify-content: space-between;
+      gap: 20px;
+      margin: 4px 0 16px;
+    }
+
+    .section-header h2 {
+      margin-bottom: 4px;
+      color: #101b33;
+      font-size: 24px;
+    }
+
+    .section-header p {
+      margin-bottom: 0;
+      color: #8a93a6;
+    }
+
+    .filter-card {
+      margin-bottom: 18px;
+      padding: 16px 18px;
+      border-radius: 22px;
+      background: #fff;
+      box-shadow: 0 16px 36px rgba(38, 51, 96, 0.06);
+      display: flex;
+      flex-wrap: wrap;
+      gap: 12px;
+      align-items: center;
+    }
+
+    .field {
+      display: inline-flex;
+      align-items: center;
+      gap: 8px;
+      padding: 3px;
+      border-radius: 15px;
+      background: #f6f8ff;
+      color: #5d667a;
+      font-size: 13px;
+      font-weight: 700;
+    }
+
+    .segmented span,
+    .date-chip,
+    .btn {
+      height: 32px;
+      padding: 0 14px;
+      border-radius: 11px;
+      display: inline-flex;
+      align-items: center;
+      justify-content: center;
+      white-space: nowrap;
+    }
+
+    .segmented span.active {
+      background: #6267f2;
+      color: #fff;
+      box-shadow: 0 8px 18px rgba(98, 103, 242, 0.22);
+    }
+
+    .date-chip {
+      min-width: 300px;
+      background: #fff;
+      color: #29364f;
+      box-shadow: inset 0 0 0 1px rgba(98, 103, 242, 0.12);
+      justify-content: space-between;
+    }
+
+    .btn {
+      border: 0;
+      background: #f6f8ff;
+      color: #6e778b;
+      font-weight: 800;
+    }
+
+    .btn.primary {
+      background: #6267f2;
+      color: #fff;
+      box-shadow: 0 8px 18px rgba(98, 103, 242, 0.22);
+    }
+
+    .btn.warn {
+      background: #fff6e8;
+      color: #f39a20;
+    }
+
+    .grid {
+      display: grid;
+      gap: 18px;
+    }
+
+    .kpi-grid {
+      grid-template-columns: repeat(4, minmax(0, 1fr));
+      margin-bottom: 18px;
+    }
+
+    .kpi-card {
+      min-height: 142px;
+      padding: 20px;
+      border-radius: 20px;
+      background: #fff;
+      box-shadow: 0 16px 34px rgba(37, 50, 88, 0.08);
+      position: relative;
+      overflow: hidden;
+      display: flex;
+      flex-direction: column;
+      justify-content: flex-end;
+    }
+
+    .kpi-card::after {
+      content: "";
+      position: absolute;
+      top: -36px;
+      right: -30px;
+      width: 108px;
+      height: 108px;
+      border-radius: 50%;
+      background: rgba(98, 103, 242, 0.1);
+    }
+
+    .kpi-card.primary {
+      background: linear-gradient(145deg, #7935f5 0%, #6028ee 100%);
+      color: #fff;
+      box-shadow: 0 20px 36px rgba(99, 50, 236, 0.24);
+    }
+
+    .kpi-icon {
+      position: absolute;
+      top: 18px;
+      left: 20px;
+      width: 44px;
+      height: 44px;
+      border-radius: 15px;
+      background: #f1efff;
+      color: #6267f2;
+      display: grid;
+      place-items: center;
+      font-weight: 900;
+    }
+
+    .primary .kpi-icon {
+      background: #fff;
+      color: #6267f2;
+    }
+
+    .kpi-label {
+      margin-bottom: 8px;
+      color: #7b8498;
+      font-size: 13px;
+    }
+
+    .primary .kpi-label,
+    .primary .kpi-hint,
+    .primary .kpi-value {
+      color: #fff;
+    }
+
+    .kpi-value {
+      color: #121d35;
+      font-size: 27px;
+      font-weight: 900;
+      line-height: 1.2;
+    }
+
+    .kpi-hint {
+      margin-top: 10px;
+      color: #a0a8b8;
+      font-size: 12px;
+    }
+
+    .two-col {
+      grid-template-columns: minmax(0, 1.4fr) minmax(320px, .6fr);
+    }
+
+    .card {
+      border-radius: 22px;
+      background: #fff;
+      box-shadow: 0 18px 42px rgba(42, 61, 115, 0.08);
+      padding: 22px;
+    }
+
+    .card h3 {
+      margin-bottom: 6px;
+      color: #101b33;
+      font-size: 18px;
+    }
+
+    .card-subtitle {
+      margin-bottom: 18px;
+      color: #8a93a6;
+      font-size: 12px;
+    }
+
+    table {
+      width: 100%;
+      border-collapse: separate;
+      border-spacing: 0;
+      overflow: hidden;
+      border-radius: 14px;
+      font-size: 13px;
+    }
+
+    th {
+      padding: 13px 14px;
+      background: #f8f9fd;
+      color: #697287;
+      text-align: left;
+      font-weight: 900;
+    }
+
+    td {
+      padding: 14px;
+      border-bottom: 1px solid #eef1f7;
+      color: #29364f;
+    }
+
+    tr:last-child td {
+      border-bottom: 0;
+    }
+
+    .rank-list {
+      display: grid;
+      gap: 12px;
+    }
+
+    .rank-item {
+      display: grid;
+      grid-template-columns: 34px minmax(0, 1fr) auto;
+      gap: 10px;
+      align-items: center;
+      padding: 11px 0;
+      border-bottom: 1px solid #f0f2f7;
+    }
+
+    .rank-item:last-child {
+      border-bottom: 0;
+    }
+
+    .rank-index {
+      width: 28px;
+      height: 28px;
+      border-radius: 10px;
+      background: #f1efff;
+      color: #6267f2;
+      display: grid;
+      place-items: center;
+      font-weight: 900;
+    }
+
+    .rank-name {
+      color: #1b2740;
+      font-weight: 800;
+    }
+
+    .rank-meta {
+      margin-top: 4px;
+      color: #9aa3b4;
+      font-size: 12px;
+    }
+
+    .rank-value {
+      color: #101b33;
+      font-weight: 900;
+    }
+
+    .mini-chart {
+      height: 260px;
+      display: flex;
+      align-items: end;
+      gap: 14px;
+      padding: 20px 10px 4px;
+      border-radius: 18px;
+      background: #f8f9fd;
+    }
+
+    .mini-chart span {
+      flex: 1;
+      min-width: 20px;
+      border-radius: 12px 12px 6px 6px;
+      background: linear-gradient(180deg, #6267f2, #8a5cf6);
+      position: relative;
+    }
+
+    .mini-chart span:nth-child(even) {
+      background: linear-gradient(180deg, #ffad55, #f39a20);
+    }
+
+    .supplier-layout {
+      grid-template-columns: repeat(3, minmax(0, 1fr));
+    }
+
+    .supplier-card {
+      min-height: 160px;
+      padding: 22px;
+      border-radius: 20px;
+      background: #fff;
+      box-shadow: 0 16px 34px rgba(37, 50, 88, 0.08);
+    }
+
+    .supplier-card h3 {
+      margin-bottom: 12px;
+      color: #101b33;
+    }
+
+    .score {
+      margin-bottom: 12px;
+      color: #6267f2;
+      font-size: 34px;
+      font-weight: 900;
+    }
+
+    .tag-row {
+      display: flex;
+      flex-wrap: wrap;
+      gap: 8px;
+    }
+
+    .tag {
+      padding: 6px 10px;
+      border-radius: 999px;
+      background: #f1efff;
+      color: #6267f2;
+      font-size: 12px;
+      font-weight: 800;
+    }
+
+    .footer-note {
+      color: #8a93a6;
+      font-size: 12px;
+      line-height: 1.7;
+      text-align: center;
+      margin: 22px 0 0;
+    }
+
+    @media (max-width: 1100px) {
+      .kpi-grid,
+      .supplier-layout {
+        grid-template-columns: repeat(2, minmax(0, 1fr));
+      }
+
+      .two-col {
+        grid-template-columns: 1fr;
+      }
+    }
+
+    @media (max-width: 720px) {
+      .preview-shell {
+        padding: 16px;
+      }
+
+      .topbar,
+      .hero,
+      .section-header {
+        align-items: flex-start;
+        flex-direction: column;
+      }
+
+      .hero-visual {
+        width: 100%;
+      }
+
+      .kpi-grid,
+      .supplier-layout {
+        grid-template-columns: 1fr;
+      }
+
+      .date-chip {
+        min-width: 220px;
+      }
+    }
+  </style>
+</head>
+<body>
+  <main class="preview-shell">
+    <div class="topbar">
+      <div class="brand">
+        <div class="brand-mark">DTM</div>
+        <span>业务监测驾驶舱预览</span>
+      </div>
+      <nav class="nav-pills">
+        <a href="#order" class="active">订单监测</a>
+        <a href="#supply">供应监测</a>
+      </nav>
+    </div>
+
+    <section class="hero">
+      <div>
+        <p class="eyebrow">DTM Data Monitor</p>
+        <h1>订单与供应链核心业务监测</h1>
+        <p>面向经营分析、渠道跟踪、供应商对比与付款计划,统一采用轻量化后台风格,重点突出指标、趋势、明细和异常。</p>
+      </div>
+      <div class="hero-visual" aria-hidden="true">
+        <span class="bar"></span>
+        <span class="bar"></span>
+        <span class="bar"></span>
+        <span class="bar"></span>
+      </div>
+    </section>
+
+    <section id="order" class="section">
+      <div class="section-header">
+        <div>
+          <h2>订单监测</h2>
+          <p>店铺运营、商品价值、渠道表现和订单价值统一分析。</p>
+        </div>
+      </div>
+
+      <div class="filter-card">
+        <div class="field segmented">
+          <strong>统计周期</strong>
+          <span>日报</span>
+          <span class="active">月报</span>
+          <span>年报</span>
+          <span>全量</span>
+        </div>
+        <div class="field">
+          <strong>日期范围</strong>
+          <span class="date-chip"><span>2022-12-01</span><span>至</span><span>2022-12-31</span></span>
+          <button class="btn primary">查询</button>
+          <button class="btn">重置</button>
+          <button class="btn warn">导出 Excel</button>
+        </div>
+      </div>
+
+      <div class="grid kpi-grid">
+        <div class="kpi-card primary">
+          <div class="kpi-icon">¥</div>
+          <div class="kpi-label">销售额</div>
+          <div class="kpi-value">¥153,298.58</div>
+          <div class="kpi-hint">当前周期累计销售</div>
+        </div>
+        <div class="kpi-card">
+          <div class="kpi-icon">单</div>
+          <div class="kpi-label">订单量</div>
+          <div class="kpi-value">488</div>
+          <div class="kpi-hint">有效订单总数</div>
+        </div>
+        <div class="kpi-card">
+          <div class="kpi-icon">件</div>
+          <div class="kpi-label">销量</div>
+          <div class="kpi-value">1,228</div>
+          <div class="kpi-hint">商品销售件数</div>
+        </div>
+        <div class="kpi-card">
+          <div class="kpi-icon">客</div>
+          <div class="kpi-label">平均客单价</div>
+          <div class="kpi-value">¥314.14</div>
+          <div class="kpi-hint">销售额 / 订单量</div>
+        </div>
+      </div>
+
+      <div class="grid two-col">
+        <div class="card">
+          <h3>渠道明细</h3>
+          <p class="card-subtitle">2022-12-01 至 2022-12-31</p>
+          <table>
+            <thead>
+              <tr>
+                <th>平台</th>
+                <th>渠道/店铺</th>
+                <th>订单量</th>
+                <th>销量</th>
+                <th>销售额</th>
+                <th>客单价</th>
+              </tr>
+            </thead>
+            <tbody>
+              <tr><td>京东自营</td><td>京东自营</td><td>60</td><td>338</td><td>¥44,176.81</td><td>¥736.28</td></tr>
+              <tr><td>佳奥旗舰店</td><td>佳奥旗舰店</td><td>63</td><td>184</td><td>¥32,623.00</td><td>¥517.83</td></tr>
+              <tr><td>传统电商平台</td><td>爱梦芽008</td><td>16</td><td>35</td><td>¥9,140.00</td><td>¥571.25</td></tr>
+              <tr><td>传统电商平台</td><td>佳奥家居专营店</td><td>32</td><td>95</td><td>¥7,299.00</td><td>¥228.09</td></tr>
+            </tbody>
+          </table>
+        </div>
+
+        <aside class="card">
+          <h3>销售 Top 5</h3>
+          <p class="card-subtitle">按销售额排序</p>
+          <div class="rank-list">
+            <div class="rank-item"><div class="rank-index">1</div><div><div class="rank-name">京东自营</div><div class="rank-meta">京东自营</div></div><div class="rank-value">¥44,176.81</div></div>
+            <div class="rank-item"><div class="rank-index">2</div><div><div class="rank-name">佳奥旗舰店</div><div class="rank-meta">旗舰渠道</div></div><div class="rank-value">¥32,623.00</div></div>
+            <div class="rank-item"><div class="rank-index">3</div><div><div class="rank-name">爱梦芽008</div><div class="rank-meta">传统电商</div></div><div class="rank-value">¥9,140.00</div></div>
+            <div class="rank-item"><div class="rank-index">4</div><div><div class="rank-name">佳奥家居专营店</div><div class="rank-meta">传统电商</div></div><div class="rank-value">¥7,299.00</div></div>
+            <div class="rank-item"><div class="rank-index">5</div><div><div class="rank-name">平台运营</div><div class="rank-meta">华东</div></div><div class="rank-value">¥4,904.00</div></div>
+          </div>
+        </aside>
+      </div>
+    </section>
+
+    <section id="supply" class="section">
+      <div class="section-header">
+        <div>
+          <h2>供应监测</h2>
+          <p>供应商查询、核心指标对比、付款计划和综合评分统一呈现。</p>
+        </div>
+      </div>
+
+      <div class="filter-card">
+        <div class="field">
+          <strong>供应商名称</strong>
+          <span class="date-chip"><span>佳奥</span><span></span><span>全部匹配</span></span>
+        </div>
+        <div class="field">
+          <strong>日期范围</strong>
+          <span class="date-chip"><span>2025-01-01</span><span>至</span><span>2025-05-14</span></span>
+          <button class="btn primary">查询</button>
+          <button class="btn">重置</button>
+        </div>
+      </div>
+
+      <div class="grid supplier-layout">
+        <div class="supplier-card">
+          <h3>供应商总数</h3>
+          <div class="score">156</div>
+          <div class="tag-row"><span class="tag">实时</span><span class="tag">覆盖采购数据</span></div>
+        </div>
+        <div class="supplier-card">
+          <h3>平均综合得分</h3>
+          <div class="score">82.5</div>
+          <div class="tag-row"><span class="tag">成本</span><span class="tag">交付</span><span class="tag">账期</span></div>
+        </div>
+        <div class="supplier-card">
+          <h3>预计付款计划</h3>
+          <div class="score">¥326.8万</div>
+          <div class="tag-row"><span class="tag">7天内到期</span><span class="tag">账期推算</span></div>
+        </div>
+      </div>
+
+      <div class="grid two-col" style="margin-top: 18px;">
+        <div class="card">
+          <h3>供应商核心指标对比</h3>
+          <p class="card-subtitle">订单金额、入库金额、交付及时率和完成率</p>
+          <div class="mini-chart" aria-label="供应商核心指标柱状图">
+            <span style="height: 72%;"></span>
+            <span style="height: 54%;"></span>
+            <span style="height: 88%;"></span>
+            <span style="height: 64%;"></span>
+            <span style="height: 46%;"></span>
+            <span style="height: 70%;"></span>
+            <span style="height: 58%;"></span>
+            <span style="height: 82%;"></span>
+          </div>
+        </div>
+        <div class="card">
+          <h3>付款计划提醒</h3>
+          <p class="card-subtitle">按预计付款日期排序</p>
+          <div class="rank-list">
+            <div class="rank-item"><div class="rank-index">1</div><div><div class="rank-name">深圳某电子有限公司</div><div class="rank-meta">2025-05-18 到期</div></div><div class="rank-value">¥86,420</div></div>
+            <div class="rank-item"><div class="rank-index">2</div><div><div class="rank-name">华东供应链服务商</div><div class="rank-meta">2025-05-21 到期</div></div><div class="rank-value">¥62,800</div></div>
+            <div class="rank-item"><div class="rank-index">3</div><div><div class="rank-name">佳奥配套工厂</div><div class="rank-meta">2025-05-28 到期</div></div><div class="rank-value">¥45,600</div></div>
+          </div>
+        </div>
+      </div>
+    </section>
+
+    <p class="footer-note">该页面为静态 HTML 预览稿,仅用于向甲方展示新版订单监测与供应监测的视觉方向和信息组织方式。</p>
+  </main>
+</body>
+</html>

Разлика између датотеке није приказан због своје велике величине
+ 2 - 0
public/qa-score-demo.html


+ 31 - 8
src/api/supply.js

@@ -27,19 +27,42 @@ export function getSupplyPaymentPlan(params) {
   })
 }
 
-// 获取产品供应商详情(成本/交付/账期)
+
+export function getSupplyPaymentSummary(params) {
+  return request({
+    url: '/api/supply/monitor/payment-summary',
+    method: 'get',
+    params,
+    timeout: 60000
+  })
+}
+
+export function getSupplyReceiptReconciliation(params) {
+  return request({
+    url: '/api/supply/monitor/receipt-reconciliation',
+    method: 'get',
+    params,
+    timeout: 60000
+  })
+}
+export function getSupplyOverviewStats() {
+  return request({
+    url: '/api/supplier/overview',
+    method: 'get',
+    timeout: 60000
+  })
+}
+// 鑾峰彇浜у搧渚涘簲鍟嗚鎯咃紙鎴愭湰/浜や粯/璐︽湡锛?
 export function getProductDetails(code) {
   return request({
-    baseURL: process.env.VUE_APP_PYTHON_API,
     url: `/api/supplier/product/${code}/details`,
     method: 'get'
   })
 }
 
-// 供应商综合评估
+// 渚涘簲鍟嗙患鍚堣瘎浼?
 export function evaluateSupplier(productCode) {
   return request({
-    baseURL: process.env.VUE_APP_PYTHON_API,
     url: '/api/supplier/evaluate',
     method: 'post',
     data: {
@@ -48,21 +71,21 @@ export function evaluateSupplier(productCode) {
   })
 }
 
-// 获取权重配置
+// 鑾峰彇鏉冮噸閰嶇疆
 export function getWeightsConfig() {
   return request({
-    baseURL: process.env.VUE_APP_PYTHON_API,
     url: '/api/supplier/config/weights',
     method: 'get'
   })
 }
 
-// 更新权重配置
+// 鏇存柊鏉冮噸閰嶇疆
 export function updateWeightsConfig(weights) {
   return request({
-    baseURL: process.env.VUE_APP_PYTHON_API,
     url: '/api/supplier/config/weights',
     method: 'put',
     data: weights
   })
 }
+
+

+ 2 - 0
src/assets/styles/index.scss

@@ -4,6 +4,8 @@
 @import './element-ui.scss';
 @import './sidebar.scss';
 @import './btn.scss';
+@import './order-dashboard.scss';
+@import './supply-dashboard.scss';
 
 body {
   height: 100%;

+ 473 - 0
src/assets/styles/order-dashboard.scss

@@ -0,0 +1,473 @@
+.order-dashboard-page {
+  min-height: calc(100vh - 84px);
+  padding: 24px !important;
+  background: #eef3fb !important;
+  color: #17243b;
+}
+
+.order-dashboard-page .page-header {
+  margin-bottom: 18px !important;
+  padding: 24px 28px !important;
+  border: 0 !important;
+  border-radius: 22px !important;
+  background: linear-gradient(110deg, #ffffff 0%, #f7f9ff 56%, #eaf0ff 100%) !important;
+  box-shadow: 0 20px 45px rgba(42, 61, 115, 0.08) !important;
+}
+
+.order-dashboard-page .main-title,
+.order-dashboard-page .page-title {
+  margin: 0 !important;
+  color: #101b33 !important;
+  font-size: 26px !important;
+  font-weight: 800 !important;
+  line-height: 1.2;
+}
+
+.order-dashboard-page .subtitle,
+.order-dashboard-page .page-subtitle,
+.order-dashboard-page .page-description {
+  margin: 10px 0 0 !important;
+  color: #7b8498 !important;
+  font-size: 14px !important;
+  line-height: 1.7;
+}
+
+.order-dashboard-page .order-date-filter {
+  margin-bottom: 18px;
+  padding: 16px 18px !important;
+  border: 0 !important;
+  border-radius: 22px !important;
+  background: #fff !important;
+  box-shadow: 0 16px 36px rgba(38, 51, 96, 0.06);
+  display: grid;
+  grid-template-columns: auto 1fr;
+  align-items: center;
+  gap: 12px;
+}
+
+.order-dashboard-page .order-date-filter .el-form-item {
+  height: 38px;
+  margin: 0;
+  padding: 3px;
+  border-radius: 15px;
+  background: #f6f8ff;
+  display: flex;
+  align-items: center;
+}
+
+.order-dashboard-page .order-date-filter .el-form-item:first-child {
+  grid-column: 1 / -1;
+  justify-self: start;
+}
+
+.order-dashboard-page .order-date-filter .el-form-item__label {
+  height: 32px;
+  padding: 0 10px 0 8px;
+  color: #5d667a;
+  font-size: 13px;
+  font-weight: 700;
+  line-height: 32px;
+}
+
+.order-dashboard-page .order-date-filter .el-form-item__content,
+.order-dashboard-page .order-date-filter .date-action-row,
+.order-dashboard-page .order-date-filter .filter-actions {
+  display: inline-flex;
+  align-items: center;
+  gap: 8px;
+  line-height: 32px;
+  white-space: nowrap;
+}
+
+.order-dashboard-page .order-date-filter .el-radio-group {
+  display: inline-flex;
+  gap: 4px;
+}
+
+.order-dashboard-page .order-date-filter .el-radio-button__inner {
+  min-width: 56px;
+  height: 32px;
+  padding: 0 14px;
+  border: 0 !important;
+  border-radius: 11px !important;
+  background: transparent;
+  color: #667086;
+  line-height: 32px;
+  box-shadow: none !important;
+}
+
+.order-dashboard-page .order-date-filter .el-radio-button__orig-radio:checked + .el-radio-button__inner {
+  background: #6267f2;
+  color: #fff;
+  box-shadow: 0 8px 18px rgba(98, 103, 242, 0.22) !important;
+}
+
+.order-dashboard-page .order-date-filter .el-date-editor.el-input__inner {
+  width: 350px;
+  height: 32px;
+  padding: 0 12px;
+  border: 0;
+  border-radius: 12px;
+  background: #fff;
+  color: #29364f;
+  box-shadow: inset 0 0 0 1px rgba(98, 103, 242, 0.12);
+}
+
+.order-dashboard-page .order-date-filter .el-date-editor .el-range-input {
+  width: 128px;
+  color: #29364f;
+  font-weight: 600;
+  background: transparent;
+}
+
+.order-dashboard-page .order-date-filter .el-date-editor .el-range-separator {
+  width: 32px;
+  color: #8c94a8;
+  line-height: 30px;
+}
+
+.order-dashboard-page .order-date-filter .el-date-editor .el-range__icon,
+.order-dashboard-page .order-date-filter .el-date-editor .el-range__close-icon {
+  color: #9aa4bc;
+  line-height: 30px;
+}
+
+.order-dashboard-page .order-date-filter .el-button {
+  height: 32px;
+  padding: 0 16px;
+  border-radius: 12px;
+  font-weight: 700;
+}
+
+.order-dashboard-page .order-date-filter .el-button--primary {
+  border-color: #6267f2;
+  background: #6267f2;
+  box-shadow: 0 8px 18px rgba(98, 103, 242, 0.22);
+}
+
+.order-dashboard-page .order-date-filter .el-button--default {
+  border-color: transparent;
+  background: #fff;
+  color: #6e778b;
+}
+
+.order-dashboard-page .order-date-filter .el-button--warning.is-plain {
+  border-color: transparent;
+  background: #fff6e8;
+  color: #f39a20;
+}
+
+.order-dashboard-page .order-date-filter .el-button + .el-button {
+  margin-left: 0;
+}
+
+.order-dashboard-page .chart-card,
+.order-dashboard-page .table-card,
+.order-dashboard-page .table-container,
+.order-dashboard-page .chart-container,
+.order-dashboard-page .chart-area,
+.order-dashboard-page .chart-wrapper,
+.order-dashboard-page .leakage-card {
+  position: relative;
+  border: 0 !important;
+  border-radius: 22px !important;
+  background: #fff !important;
+  box-shadow: 0 18px 42px rgba(42, 61, 115, 0.08) !important;
+}
+
+.order-dashboard-page .chart-card,
+.order-dashboard-page .table-card,
+.order-dashboard-page .table-container,
+.order-dashboard-page .chart-container,
+.order-dashboard-page .chart-area,
+.order-dashboard-page .chart-wrapper {
+  padding: 22px !important;
+}
+
+.order-dashboard-page .inner-chart-card {
+  border-radius: 18px !important;
+  box-shadow: inset 0 0 0 1px #eef1f7 !important;
+}
+
+.order-dashboard-page .chart-title,
+.order-dashboard-page .table-title {
+  margin: 0 0 16px !important;
+  color: #101b33 !important;
+  font-size: 18px !important;
+  font-weight: 800 !important;
+}
+
+.order-dashboard-page .card-header,
+.order-dashboard-page .table-header {
+  margin-bottom: 18px !important;
+}
+
+.order-dashboard-page .kpi-card,
+.order-dashboard-page .kpi-cards-grid .kpi-card {
+  min-height: 142px;
+  padding: 20px !important;
+  border: 0 !important;
+  border-radius: 20px !important;
+  background: #fff !important;
+  box-shadow: 0 16px 34px rgba(37, 50, 88, 0.08) !important;
+  overflow: hidden;
+}
+
+.order-dashboard-page .kpi-card-grid .kpi-card:first-child,
+.order-dashboard-page .kpi-cards-grid .kpi-card:first-of-type {
+  background: linear-gradient(145deg, #7935f5 0%, #6028ee 100%) !important;
+  box-shadow: 0 20px 36px rgba(99, 50, 236, 0.24) !important;
+}
+
+.order-dashboard-page .kpi-card-grid .kpi-card:first-child .kpi-title,
+.order-dashboard-page .kpi-card-grid .kpi-card:first-child .kpi-value,
+.order-dashboard-page .kpi-cards-grid .kpi-card:first-of-type .card-title,
+.order-dashboard-page .kpi-cards-grid .kpi-card:first-of-type .main-value,
+.order-dashboard-page .kpi-cards-grid .kpi-card:first-of-type .trend-text {
+  color: #fff !important;
+}
+
+.order-dashboard-page .kpi-title,
+.order-dashboard-page .card-title {
+  color: #7b8498 !important;
+  font-size: 13px !important;
+  font-weight: 700 !important;
+}
+
+.order-dashboard-page .kpi-value,
+.order-dashboard-page .main-value {
+  color: #121d35 !important;
+  font-size: 25px !important;
+  line-height: 1.2 !important;
+}
+
+.order-dashboard-page .icon-wrapper {
+  border-radius: 14px !important;
+  background: #f1efff !important;
+  color: #6c3df4 !important;
+}
+
+.order-dashboard-page .search-box .el-input__inner {
+  border: 0;
+  border-radius: 12px 0 0 12px;
+  background: #f6f8ff;
+}
+
+.order-dashboard-page .search-box .el-input-group__append {
+  border: 0;
+  border-radius: 0 12px 12px 0;
+  background: #6267f2;
+  color: #fff;
+}
+
+.order-dashboard-page table,
+.order-dashboard-page .data-table {
+  width: 100%;
+  overflow: hidden;
+  border-collapse: separate !important;
+  border-spacing: 0;
+  border-radius: 14px;
+}
+
+.order-dashboard-page th,
+.order-dashboard-page .data-table th {
+  border: 0 !important;
+  background: #f8f9fd !important;
+  color: #697287 !important;
+  font-weight: 800 !important;
+}
+
+.order-dashboard-page td,
+.order-dashboard-page .data-table td {
+  border: 0 !important;
+  border-bottom: 1px solid #eef1f7 !important;
+  color: #29364f;
+}
+
+.order-dashboard-page tr:hover td,
+.order-dashboard-page .data-row:hover {
+  background: #f8f9ff !important;
+}
+
+.order-dashboard-page .pagination,
+.order-dashboard-page .pagination-controls {
+  margin-top: 18px !important;
+  display: flex;
+  align-items: center;
+  justify-content: flex-end;
+  gap: 12px;
+}
+
+.order-dashboard-page .pagination button,
+.order-dashboard-page .pagination-controls button,
+.order-dashboard-page .page-button {
+  height: 34px;
+  padding: 0 14px !important;
+  border: 0 !important;
+  border-radius: 12px !important;
+  background: #f6f8ff !important;
+  color: #606b82 !important;
+  font-weight: 700;
+}
+
+.order-dashboard-page .pagination button:hover:not(:disabled),
+.order-dashboard-page .pagination-controls button:hover:not(:disabled),
+.order-dashboard-page .page-button:hover:not(:disabled) {
+  background: #6267f2 !important;
+  color: #fff !important;
+}
+
+.order-dashboard-page .status-overlay {
+  border-radius: 22px !important;
+  background: rgba(255, 255, 255, 0.9) !important;
+}
+
+.order-dashboard-page .order-loading-panel {
+  border-radius: inherit;
+}
+
+.order-date-popper,
+.shop-report-date-popper {
+  width: 646px;
+  border: 0 !important;
+  border-radius: 18px !important;
+  background: #fff;
+  box-shadow: 0 24px 60px rgba(32, 45, 82, 0.16) !important;
+  overflow: hidden !important;
+}
+
+.order-date-popper .popper__arrow,
+.shop-report-date-popper .popper__arrow {
+  display: none;
+}
+
+.order-date-popper .el-picker-panel__body,
+.shop-report-date-popper .el-picker-panel__body {
+  min-width: 646px;
+}
+
+.order-date-popper .el-date-range-picker__content,
+.shop-report-date-popper .el-date-range-picker__content {
+  width: 50%;
+  padding: 20px 22px 18px;
+}
+
+.order-date-popper .el-date-range-picker__content.is-left,
+.shop-report-date-popper .el-date-range-picker__content.is-left {
+  border-right-color: #eef1f7;
+}
+
+.order-date-popper .el-date-range-picker__header,
+.shop-report-date-popper .el-date-range-picker__header {
+  height: 32px;
+  margin-bottom: 12px;
+  line-height: 32px;
+}
+
+.order-date-popper .el-date-range-picker__header div,
+.shop-report-date-popper .el-date-range-picker__header div {
+  color: #24314a;
+  font-size: 16px;
+  font-weight: 700;
+}
+
+.order-date-popper .el-picker-panel__icon-btn,
+.shop-report-date-popper .el-picker-panel__icon-btn {
+  color: #8c94a8;
+  font-size: 13px;
+  line-height: 32px;
+}
+
+.order-date-popper .el-date-table th,
+.shop-report-date-popper .el-date-table th {
+  padding: 6px 0 10px;
+  color: #7d879b;
+  font-weight: 700;
+  border-bottom: 0;
+}
+
+.order-date-popper .el-date-table td,
+.shop-report-date-popper .el-date-table td {
+  height: 36px;
+  padding: 2px 0;
+  color: #26364f;
+}
+
+.order-date-popper .el-date-table td div,
+.shop-report-date-popper .el-date-table td div {
+  height: 32px;
+  padding: 2px 0;
+  border-radius: 10px;
+}
+
+.order-date-popper .el-date-table td span,
+.shop-report-date-popper .el-date-table td span {
+  width: 28px;
+  height: 28px;
+  border-radius: 9px;
+  line-height: 28px;
+}
+
+.order-date-popper .el-date-table td.disabled div,
+.shop-report-date-popper .el-date-table td.disabled div {
+  background: transparent;
+}
+
+.order-date-popper .el-date-table td.disabled span,
+.order-date-popper .el-date-table td.prev-month span,
+.order-date-popper .el-date-table td.next-month span,
+.shop-report-date-popper .el-date-table td.disabled span,
+.shop-report-date-popper .el-date-table td.prev-month span,
+.shop-report-date-popper .el-date-table td.next-month span {
+  color: #b7bfce;
+}
+
+.order-date-popper .el-date-table td.in-range div,
+.order-date-popper .el-date-table td.in-range div:hover,
+.shop-report-date-popper .el-date-table td.in-range div,
+.shop-report-date-popper .el-date-table td.in-range div:hover {
+  background: #f4f6ff;
+}
+
+.order-date-popper .el-date-table td.available:hover span,
+.shop-report-date-popper .el-date-table td.available:hover span {
+  background: #f0f2ff;
+  color: #6267f2;
+}
+
+.order-date-popper .el-date-table td.start-date span,
+.order-date-popper .el-date-table td.end-date span,
+.order-date-popper .el-date-table td.current:not(.disabled) span,
+.shop-report-date-popper .el-date-table td.start-date span,
+.shop-report-date-popper .el-date-table td.end-date span,
+.shop-report-date-popper .el-date-table td.current:not(.disabled) span {
+  background: #6267f2;
+  color: #fff;
+  box-shadow: 0 8px 16px rgba(98, 103, 242, 0.24);
+}
+
+@media (max-width: 768px) {
+  .order-dashboard-page {
+    padding: 14px !important;
+  }
+
+  .order-dashboard-page .order-date-filter {
+    grid-template-columns: 1fr;
+  }
+
+  .order-dashboard-page .order-date-filter .el-form-item {
+    width: 100%;
+    height: auto;
+    align-items: flex-start;
+    flex-direction: column;
+  }
+
+  .order-dashboard-page .order-date-filter .date-action-row {
+    width: 100%;
+    flex-wrap: wrap;
+  }
+
+  .order-dashboard-page .order-date-filter .el-date-editor.el-input__inner {
+    width: 100%;
+  }
+}

+ 349 - 0
src/assets/styles/supply-dashboard.scss

@@ -0,0 +1,349 @@
+.supply-dashboard-page {
+  min-height: calc(100vh - 84px);
+  padding: 24px !important;
+  background: #eef3fb !important;
+  color: #17243b;
+}
+
+.supply-dashboard-page .page-header {
+  margin-bottom: 18px !important;
+  padding: 24px 28px !important;
+  border: 0 !important;
+  border-radius: 22px !important;
+  background: linear-gradient(110deg, #ffffff 0%, #f7f9ff 56%, #eaf0ff 100%) !important;
+  box-shadow: 0 20px 45px rgba(42, 61, 115, 0.08) !important;
+}
+
+.supply-dashboard-page .page-header h2,
+.supply-dashboard-page .page-title {
+  margin: 0 !important;
+  color: #101b33 !important;
+  font-size: 26px !important;
+  font-weight: 800 !important;
+  line-height: 1.2;
+  background: none !important;
+  -webkit-text-fill-color: currentColor !important;
+}
+
+.supply-dashboard-page .page-header h2 i {
+  width: 42px;
+  height: 42px;
+  margin-right: 12px !important;
+  border-radius: 14px;
+  background: #f1efff;
+  color: #6267f2 !important;
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.supply-dashboard-page .page-desc,
+.supply-dashboard-page .page-subtitle {
+  margin: 10px 0 0 !important;
+  color: #7b8498 !important;
+  font-size: 14px !important;
+  line-height: 1.7;
+}
+
+.supply-dashboard-page .box-card,
+.supply-dashboard-page .content-card,
+.supply-dashboard-page .stat-card,
+.supply-dashboard-page .info-box,
+.supply-dashboard-page .chart-card,
+.supply-dashboard-page .el-tabs--border-card {
+  border: 0 !important;
+  border-radius: 22px !important;
+  background: #fff !important;
+  box-shadow: 0 18px 42px rgba(42, 61, 115, 0.08) !important;
+}
+
+.supply-dashboard-page .box-card,
+.supply-dashboard-page .content-card {
+  margin-bottom: 18px !important;
+}
+
+.supply-dashboard-page .el-card__header {
+  padding: 18px 22px;
+  border-bottom: 1px solid #eef1f7 !important;
+  color: #101b33;
+  font-size: 16px;
+  font-weight: 800 !important;
+}
+
+.supply-dashboard-page .el-card__body {
+  padding: 22px;
+}
+
+.supply-dashboard-page .form-inline {
+  gap: 10px;
+  align-items: center;
+}
+
+.supply-dashboard-page .el-form-item {
+  margin-right: 10px;
+  margin-bottom: 10px;
+}
+
+.supply-dashboard-page .el-form-item__label {
+  color: #5d667a;
+  font-weight: 700;
+}
+
+.supply-dashboard-page .el-input__inner,
+.supply-dashboard-page .el-date-editor.el-input__inner {
+  height: 36px;
+  border: 0;
+  border-radius: 12px;
+  background: #f6f8ff;
+  color: #29364f;
+  box-shadow: inset 0 0 0 1px rgba(98, 103, 242, 0.1);
+}
+
+.supply-dashboard-page .el-date-editor .el-range-input {
+  background: transparent;
+  color: #29364f;
+  font-weight: 600;
+}
+
+.supply-dashboard-page .el-date-editor .el-range-separator {
+  color: #8c94a8;
+}
+
+.supply-dashboard-page .el-button {
+  border-radius: 12px;
+  font-weight: 700;
+}
+
+.supply-dashboard-page .el-button--primary {
+  border-color: #6267f2;
+  background: #6267f2;
+  box-shadow: 0 8px 18px rgba(98, 103, 242, 0.2);
+}
+
+.supply-dashboard-page .el-button--default {
+  border-color: transparent;
+  background: #f6f8ff;
+  color: #6e778b;
+}
+
+.supply-dashboard-page .el-button--text {
+  color: #6267f2;
+}
+
+.supply-dashboard-page .el-alert {
+  border: 0;
+  border-radius: 16px;
+}
+
+.supply-dashboard-page .el-tabs--border-card {
+  overflow: hidden;
+}
+
+.supply-dashboard-page .el-tabs--border-card > .el-tabs__header {
+  border: 0;
+  background: #f8f9fd;
+}
+
+.supply-dashboard-page .el-tabs--border-card > .el-tabs__header .el-tabs__item {
+  border: 0;
+  color: #6e778b;
+  font-weight: 700;
+}
+
+.supply-dashboard-page .el-tabs--border-card > .el-tabs__header .el-tabs__item.is-active {
+  background: #fff;
+  color: #6267f2;
+}
+
+.supply-dashboard-page .el-table {
+  border-radius: 14px;
+  overflow: hidden;
+}
+
+.supply-dashboard-page .el-table th.el-table__cell {
+  background: #f8f9fd !important;
+  color: #697287;
+  font-weight: 800;
+}
+
+.supply-dashboard-page .el-table td.el-table__cell,
+.supply-dashboard-page .el-table th.el-table__cell.is-leaf {
+  border-bottom-color: #eef1f7;
+}
+
+.supply-dashboard-page .el-table--border,
+.supply-dashboard-page .el-table--group {
+  border-color: #eef1f7;
+}
+
+.supply-dashboard-page .el-table--border::after,
+.supply-dashboard-page .el-table--group::after,
+.supply-dashboard-page .el-table::before {
+  background: transparent;
+}
+
+.supply-dashboard-page .toolbar {
+  padding: 14px 16px;
+  margin-bottom: 16px !important;
+  border-radius: 16px;
+  background: #f8f9fd;
+  color: #6e778b !important;
+}
+
+.supply-dashboard-page .chart-card {
+  padding: 20px !important;
+}
+
+.supply-dashboard-page .chart-title,
+.supply-dashboard-page .card-title {
+  margin-bottom: 16px !important;
+  color: #101b33 !important;
+  font-size: 18px !important;
+  font-weight: 800 !important;
+}
+
+.supply-dashboard-page .chart-empty,
+.supply-dashboard-page .no-data,
+.supply-dashboard-page .loading {
+  color: #8a93a6 !important;
+}
+
+.supply-dashboard-page .pagination-wrap {
+  padding-top: 18px !important;
+}
+
+.supply-dashboard-page .el-pagination .btn-prev,
+.supply-dashboard-page .el-pagination .btn-next,
+.supply-dashboard-page .el-pagination .el-pager li {
+  border-radius: 10px;
+}
+
+.supply-dashboard-page .el-pagination .el-pager li.active {
+  color: #6267f2;
+}
+
+.supply-dashboard-page .stats-grid {
+  gap: 18px !important;
+  margin-bottom: 18px !important;
+}
+
+.supply-dashboard-page .stat-card {
+  padding: 22px !important;
+  border: 0 !important;
+}
+
+.supply-dashboard-page .stat-card::before {
+  background: linear-gradient(90deg, #6574f7 0%, #6c3df4 100%) !important;
+}
+
+.supply-dashboard-page .stat-value,
+.supply-dashboard-page .info-value {
+  color: #121d35 !important;
+}
+
+.supply-dashboard-page .stat-badge,
+.supply-dashboard-page .dim-score {
+  border: 0 !important;
+  border-radius: 999px !important;
+  background: #f1efff !important;
+  color: #6267f2 !important;
+}
+
+.supply-dashboard-page .content-card {
+  padding: 24px !important;
+  border: 0 !important;
+}
+
+.supply-dashboard-page .content-card:hover,
+.supply-dashboard-page .stat-card:hover,
+.supply-dashboard-page .info-box:hover {
+  transform: none !important;
+}
+
+.supply-dashboard-page .search-input {
+  border: 0 !important;
+  border-radius: 14px !important;
+  background: #f6f8ff !important;
+  box-shadow: inset 0 0 0 1px rgba(98, 103, 242, 0.1);
+}
+
+.supply-dashboard-page .search-input:focus {
+  box-shadow: inset 0 0 0 1px #6267f2, 0 0 0 4px rgba(98, 103, 242, 0.08) !important;
+}
+
+.supply-dashboard-page .search-btn {
+  border-radius: 14px !important;
+  background: #6267f2 !important;
+  box-shadow: 0 8px 18px rgba(98, 103, 242, 0.2) !important;
+}
+
+.supply-dashboard-page .chart-placeholder {
+  border-radius: 20px !important;
+  background: linear-gradient(110deg, #6574f7 0%, #6c3df4 100%) !important;
+  box-shadow: 0 18px 42px rgba(98, 103, 242, 0.16) !important;
+}
+
+.supply-dashboard-page .info-box {
+  padding: 20px !important;
+  border: 0 !important;
+  background: #f8f9fd !important;
+}
+
+.supply-dashboard-page .supplier-table {
+  box-shadow: none !important;
+}
+
+.supply-dashboard-page .supplier-table th {
+  border-bottom: 0 !important;
+  background: #f8f9fd !important;
+  color: #697287 !important;
+}
+
+.supply-dashboard-page .supplier-table td {
+  border-bottom-color: #eef1f7 !important;
+}
+
+.supply-dashboard-page .progress-bar {
+  background: #edf0fb !important;
+}
+
+.supply-dashboard-page .progress-fill {
+  background: #6267f2 !important;
+  box-shadow: none !important;
+}
+
+.supply-dashboard-page .weight-summary {
+  border-left: 0 !important;
+  border-radius: 16px !important;
+  background: #f8f9fd !important;
+}
+
+.supply-dashboard-page .el-slider__bar {
+  background: #6267f2;
+}
+
+.supply-dashboard-page .el-slider__button {
+  border-color: #6267f2;
+}
+
+@media (max-width: 768px) {
+  .supply-dashboard-page {
+    padding: 14px !important;
+  }
+
+  .supply-dashboard-page .page-header {
+    padding: 22px !important;
+  }
+
+  .supply-dashboard-page .form-inline,
+  .supply-dashboard-page .search-box {
+    flex-direction: column;
+    align-items: stretch;
+  }
+
+  .supply-dashboard-page .el-input,
+  .supply-dashboard-page .el-date-editor,
+  .supply-dashboard-page .search-btn {
+    width: 100% !important;
+  }
+}

+ 6 - 0
src/router/index.js

@@ -128,6 +128,12 @@ export const constantRoutes = [
         component: () => import('@/views/supply/monitor/index.vue'),
         name: 'SupplyMonitorEnhancement',
         meta: { title: '供应商查询对比与付款计划', icon: 'chart' }
+      },
+      {
+        path: 'qa-demo',
+        component: () => import('@/views/supply/qa-demo/index.vue'),
+        name: 'SupplyQaScoreDemo',
+        meta: { title: 'QA??????', icon: 'chart' }
       }
     ]
   }

+ 1 - 1
src/views/order/channel/index.vue

@@ -1,5 +1,5 @@
 <template>
-  <div class="page-container">
+  <div class="page-container order-dashboard-page">
     <header class="page-header">
       <h1 class="main-title">商品渠道透视</h1>
       <p class="subtitle">分析商品在不同销售渠道的覆盖广度</p>

+ 12 - 5
src/views/order/components/OrderDateRangeFilter.vue

@@ -9,9 +9,11 @@
       </el-radio-group>
     </el-form-item>
     <el-form-item label="日期范围">
-      <el-date-picker
+      <span class="date-action-row">
+        <el-date-picker
         v-model="dateRange"
         type="daterange"
+        :popper-class="popperClass"
         value-format="yyyy-MM-dd"
         range-separator="至"
         start-placeholder="开始日期"
@@ -20,12 +22,13 @@
         :disabled="periodType === 'all'"
         :editable="false"
         :picker-options="pickerOptions"
-      />
-    </el-form-item>
-    <el-form-item>
+        />
+        <span class="filter-actions">
       <el-button type="primary" icon="el-icon-search" size="mini" @click="emitChange">查询</el-button>
       <el-button icon="el-icon-refresh" size="mini" @click="reset">重置</el-button>
-      <slot name="actions" :params="queryParams" />
+          <slot name="actions" :params="queryParams" />
+        </span>
+      </span>
     </el-form-item>
   </el-form>
 </template>
@@ -49,6 +52,10 @@ export default {
     fullRangeStart: {
       type: String,
       default: '1900-01-01'
+    },
+    popperClass: {
+      type: String,
+      default: 'order-date-popper'
     }
   },
   data() {

+ 33 - 48
src/views/order/components/OrderLoadingPanel.vue

@@ -1,18 +1,17 @@
 <template>
   <div :class="['order-loading-panel', { compact }]">
-    <div class="loading-visual">
+    <div class="loading-visual" aria-hidden="true">
       <span class="orbit orbit-one"></span>
       <span class="orbit orbit-two"></span>
       <span class="center-dot"></span>
     </div>
     <div class="loading-copy">
       <p class="loading-title">{{ title }}</p>
-      <p class="loading-detail">{{ detail }}</p>
     </div>
     <div class="progress-track">
       <span class="progress-bar"></span>
     </div>
-    <div class="pulse-row">
+    <div class="pulse-row" aria-hidden="true">
       <span></span>
       <span></span>
       <span></span>
@@ -50,16 +49,14 @@ export default {
   flex-direction: column;
   align-items: center;
   justify-content: center;
-  gap: 14px;
+  gap: 0;
   min-height: 220px;
   padding: 24px;
   border-radius: inherit;
-  background:
-    linear-gradient(135deg, rgba(255, 255, 255, 0.96), rgba(247, 251, 255, 0.92)),
-    radial-gradient(circle at 35% 30%, rgba(24, 141, 240, 0.12), transparent 34%);
-  color: #27364a;
+  background: rgba(255, 255, 255, 0.78);
+  color: #17243b;
   text-align: center;
-  backdrop-filter: blur(3px);
+  backdrop-filter: blur(1px);
 }
 
 .order-loading-panel.compact {
@@ -69,69 +66,61 @@ export default {
 }
 
 .loading-visual {
-  position: relative;
-  width: 64px;
-  height: 64px;
+  display: none;
 }
 
 .compact .loading-visual {
-  width: 46px;
-  height: 46px;
+  width: 34px;
+  height: 34px;
 }
 
 .orbit {
   position: absolute;
   inset: 0;
   border-radius: 50%;
-  border: 3px solid transparent;
+  border: 3px solid #edf0fb;
 }
 
 .orbit-one {
-  border-top-color: #188df0;
-  border-right-color: rgba(24, 141, 240, 0.24);
-  animation: spin 1s linear infinite;
+  border-top-color: #6267f2;
+  animation: spin 0.95s linear infinite;
 }
 
 .orbit-two {
-  inset: 10px;
-  border-left-color: #4ecdc4;
-  border-bottom-color: rgba(78, 205, 196, 0.25);
-  animation: spin 1.4s linear infinite reverse;
+  display: none;
 }
 
 .center-dot {
-  position: absolute;
-  inset: 24px;
-  border-radius: 50%;
-  background: linear-gradient(135deg, #188df0, #4ecdc4);
-  box-shadow: 0 0 18px rgba(24, 141, 240, 0.36);
+  display: none;
 }
 
 .compact .center-dot {
-  inset: 17px;
+  inset: 16px;
 }
 
 .loading-copy {
   display: flex;
   flex-direction: column;
-  gap: 4px;
+  gap: 0;
 }
 
 .loading-title {
   margin: 0;
-  font-size: 16px;
-  font-weight: 600;
-  color: #1f2d3d;
+  padding: 8px 14px;
+  border-radius: 999px;
+  background: rgba(255, 255, 255, 0.72);
+  color: #8a93a6;
+  font-size: 13px;
+  font-weight: 700;
+  box-shadow: 0 10px 24px rgba(42, 61, 115, 0.08);
 }
 
 .compact .loading-title {
-  font-size: 14px;
+  font-size: 12px;
 }
 
 .loading-detail {
-  margin: 0;
-  font-size: 13px;
-  color: #7a8797;
+  display: none;
 }
 
 .compact .loading-detail {
@@ -139,33 +128,29 @@ export default {
 }
 
 .progress-track {
-  width: min(280px, 70%);
-  height: 7px;
-  overflow: hidden;
-  border-radius: 999px;
-  background: #e9f2fb;
+  display: none;
 }
 
 .progress-bar {
   display: block;
-  width: 45%;
+  width: 38%;
   height: 100%;
   border-radius: inherit;
-  background: linear-gradient(90deg, #188df0, #4ecdc4);
-  animation: progressMove 1.35s ease-in-out infinite;
+  background: #6267f2;
+  box-shadow: none;
+  animation: progressMove 1.25s ease-in-out infinite;
 }
 
 .pulse-row {
-  display: flex;
-  gap: 7px;
+  display: none;
 }
 
 .pulse-row span {
   width: 7px;
   height: 7px;
   border-radius: 50%;
-  background: #188df0;
-  opacity: 0.35;
+  background: #6267f2;
+  opacity: 0.32;
   animation: pulse 1.1s ease-in-out infinite;
 }
 

+ 1 - 1
src/views/order/efficiency/index.vue

@@ -1,5 +1,5 @@
 <template>
-  <div class="department-efficiency-view">
+  <div class="department-efficiency-view order-dashboard-page">
     <header class="page-header">
       <h1 class="page-title">综合运营分析</h1>
       <p class="page-description">分析各部门的平均销售额与渠道商品多样性。</p>

+ 1 - 1
src/views/order/ordervalue/index.vue

@@ -1,5 +1,5 @@
 <template>
-  <div class="order-value-view">
+  <div class="order-value-view order-dashboard-page">
     <header class="page-header">
       <h1 class="page-title">订单价值</h1>
     </header>

+ 1 - 1
src/views/order/related/index.vue

@@ -1,5 +1,5 @@
 <template>
-  <div class="product-analysis-view">
+  <div class="product-analysis-view order-dashboard-page">
     <header class="page-header">
       <h1 class="page-title">商品关联透视</h1>
       <p class="page-subtitle">探索商品之间的共同购买关系,发现潜在高价值组合。</p>

+ 877 - 82
src/views/order/shopreport/index.vue

@@ -1,76 +1,136 @@
 <template>
-  <div class="app-container shop-report-page">
-    <OrderDateRangeFilter
-      :max-date="maxDate"
-      default-period="day"
-      storage-key="dtm-shop-date-filter"
-      class="report-toolbar"
-      @change="handleFilterChange"
-    >
-      <template slot="actions">
-        <el-button type="warning" plain icon="el-icon-download" size="mini" @click="handleExport">导出 Excel</el-button>
-      </template>
-    </OrderDateRangeFilter>
-
-    <el-alert
-      class="report-alert"
-      title="当前没有成本/费用数据源,毛利额和毛利率字段已预留,暂不参与计算。"
-      type="warning"
-      :closable="false"
-      show-icon
-    />
-
-    <el-row :gutter="16" class="summary-row">
-      <el-col :xs="24" :sm="12" :md="6">
-        <div class="summary-item">
-          <span class="summary-label">销售额</span>
-          <strong>¥{{ formatMoney(summary.salesAmount) }}</strong>
+  <div class="app-container shop-report-page order-dashboard-page">
+    <div class="report-shell">
+      <section class="report-main">
+        <div class="hero-panel">
+          <div>
+            <p class="eyebrow">Order Monitor</p>
+            <h2>店铺运营报表</h2>
+            <p class="hero-desc">聚合平台、渠道与店铺的销售表现,快速查看订单规模、销量与客单价。</p>
+          </div>
+          <div class="hero-visual">
+            <span class="hero-icon el-icon-s-data" />
+            <div class="hero-bars">
+              <i />
+              <i />
+              <i />
+              <i />
+            </div>
+          </div>
         </div>
-      </el-col>
-      <el-col :xs="24" :sm="12" :md="6">
-        <div class="summary-item">
-          <span class="summary-label">订单量</span>
-          <strong>{{ formatNumber(summary.orderCount) }}</strong>
-        </div>
-      </el-col>
-      <el-col :xs="24" :sm="12" :md="6">
-        <div class="summary-item">
-          <span class="summary-label">销量</span>
-          <strong>{{ formatNumber(summary.totalQuantity) }}</strong>
+
+        <OrderDateRangeFilter
+          :max-date="maxDate"
+          default-period="day"
+          storage-key="dtm-shop-date-filter"
+          popper-class="shop-report-date-popper"
+          class="report-toolbar"
+          @change="handleFilterChange"
+        >
+          <template slot="actions">
+            <el-button type="warning" plain icon="el-icon-download" size="mini" @click="handleExport">导出 Excel</el-button>
+          </template>
+        </OrderDateRangeFilter>
+
+        <el-alert
+          class="report-alert"
+          title="当前没有成本/费用数据源,毛利额和毛利率字段已预留,暂不参与计算。"
+          type="warning"
+          :closable="false"
+          show-icon
+        />
+
+        <el-row :gutter="18" class="summary-row">
+          <el-col v-for="card in summaryCards" :key="card.key" :xs="24" :sm="12" :md="6">
+            <div class="summary-item" :class="card.className">
+              <span class="summary-icon" :class="card.icon" />
+              <span class="summary-label">{{ card.label }}</span>
+              <strong>{{ card.value }}</strong>
+              <small>{{ card.hint }}</small>
+            </div>
+          </el-col>
+        </el-row>
+
+        <section class="table-card">
+          <div class="section-heading">
+            <div>
+              <h3>渠道明细</h3>
+              <p>{{ dateRangeLabel }}</p>
+            </div>
+            <el-button type="primary" icon="el-icon-refresh" size="mini" plain @click="handleQuery">刷新</el-button>
+          </div>
+
+          <el-table v-loading="loading" :data="reportList" class="report-table">
+            <el-table-column label="统计周期" prop="period" min-width="110" fixed="left" />
+            <el-table-column label="平台" prop="platformName" min-width="140" show-overflow-tooltip />
+            <el-table-column label="渠道/店铺" prop="channelName" min-width="160" show-overflow-tooltip />
+            <el-table-column label="订单量" prop="orderCount" min-width="100" align="right">
+              <template slot-scope="scope">{{ formatNumber(scope.row.orderCount) }}</template>
+            </el-table-column>
+            <el-table-column label="销量" prop="totalQuantity" min-width="100" align="right">
+              <template slot-scope="scope">{{ formatNumber(scope.row.totalQuantity) }}</template>
+            </el-table-column>
+            <el-table-column label="销售额" prop="salesAmount" min-width="130" align="right">
+              <template slot-scope="scope">¥{{ formatMoney(scope.row.salesAmount) }}</template>
+            </el-table-column>
+            <el-table-column label="客单价" prop="avgOrderValue" min-width="120" align="right">
+              <template slot-scope="scope">¥{{ formatMoney(scope.row.avgOrderValue) }}</template>
+            </el-table-column>
+            <el-table-column label="毛利额" prop="grossProfit" min-width="100" align="center">
+              <template slot-scope="scope">{{ scope.row.grossProfit || '--' }}</template>
+            </el-table-column>
+            <el-table-column label="毛利率" prop="grossMargin" min-width="100" align="center">
+              <template slot-scope="scope">{{ scope.row.grossMargin || '--' }}</template>
+            </el-table-column>
+            <el-table-column label="备注" prop="remark" min-width="260" show-overflow-tooltip />
+          </el-table>
+        </section>
+      </section>
+
+      <aside class="report-side">
+        <div class="side-card performance-card">
+          <div class="section-heading compact">
+            <div>
+              <h3>销售概览</h3>
+              <p>当前筛选范围</p>
+            </div>
+          </div>
+          <div class="gauge">
+            <div class="gauge-arc" />
+            <strong>{{ formatNumber(summary.orderCount) }}</strong>
+            <span>总订单量</span>
+          </div>
+          <div class="side-metric">
+            <span>平均每单件数</span>
+            <strong>{{ formatNumber(avgItemsPerOrder) }}</strong>
+          </div>
+          <div class="side-metric">
+            <span>覆盖渠道/店铺</span>
+            <strong>{{ formatNumber(reportList.length) }}</strong>
+          </div>
         </div>
-      </el-col>
-      <el-col :xs="24" :sm="12" :md="6">
-        <div class="summary-item">
-          <span class="summary-label">平均客单价</span>
-          <strong>¥{{ formatMoney(summary.avgOrderValue) }}</strong>
+
+        <div class="side-card">
+          <div class="section-heading compact">
+            <div>
+              <h3>销售 Top 5</h3>
+              <p>按销售额排序</p>
+            </div>
+          </div>
+          <div v-if="topChannels.length" class="rank-list">
+            <div v-for="(item, index) in topChannels" :key="`${item.channelName}-${index}`" class="rank-item">
+              <span class="rank-index">{{ index + 1 }}</span>
+              <div>
+                <strong>{{ item.channelName || item.platformName || '未命名渠道' }}</strong>
+                <small>{{ item.platformName || '暂无平台' }}</small>
+              </div>
+              <em>¥{{ formatMoney(item.salesAmount) }}</em>
+            </div>
+          </div>
+          <el-empty v-else description="暂无数据" :image-size="72" />
         </div>
-      </el-col>
-    </el-row>
-
-    <el-table v-loading="loading" :data="reportList" border stripe class="report-table">
-      <el-table-column label="统计周期" prop="period" min-width="110" fixed="left" />
-      <el-table-column label="平台" prop="platformName" min-width="140" show-overflow-tooltip />
-      <el-table-column label="渠道/店铺" prop="channelName" min-width="160" show-overflow-tooltip />
-      <el-table-column label="订单量" prop="orderCount" min-width="100" align="right">
-        <template slot-scope="scope">{{ formatNumber(scope.row.orderCount) }}</template>
-      </el-table-column>
-      <el-table-column label="销量" prop="totalQuantity" min-width="100" align="right">
-        <template slot-scope="scope">{{ formatNumber(scope.row.totalQuantity) }}</template>
-      </el-table-column>
-      <el-table-column label="销售额" prop="salesAmount" min-width="130" align="right">
-        <template slot-scope="scope">¥{{ formatMoney(scope.row.salesAmount) }}</template>
-      </el-table-column>
-      <el-table-column label="客单价" prop="avgOrderValue" min-width="120" align="right">
-        <template slot-scope="scope">¥{{ formatMoney(scope.row.avgOrderValue) }}</template>
-      </el-table-column>
-      <el-table-column label="毛利额" prop="grossProfit" min-width="100" align="center">
-        <template slot-scope="scope">{{ scope.row.grossProfit || '--' }}</template>
-      </el-table-column>
-      <el-table-column label="毛利率" prop="grossMargin" min-width="100" align="center">
-        <template slot-scope="scope">{{ scope.row.grossMargin || '--' }}</template>
-      </el-table-column>
-      <el-table-column label="备注" prop="remark" min-width="260" show-overflow-tooltip />
-    </el-table>
+      </aside>
+    </div>
   </div>
 </template>
 
@@ -101,6 +161,56 @@ export default {
       }, { orderCount: 0, totalQuantity: 0, salesAmount: 0 })
       total.avgOrderValue = total.orderCount > 0 ? total.salesAmount / total.orderCount : 0
       return total
+    },
+    summaryCards() {
+      return [
+        {
+          key: 'sales',
+          label: '销售额',
+          value: `¥${this.formatMoney(this.summary.salesAmount)}`,
+          hint: '当前周期累计销售',
+          icon: 'el-icon-wallet',
+          className: 'is-primary'
+        },
+        {
+          key: 'orders',
+          label: '订单量',
+          value: this.formatNumber(this.summary.orderCount),
+          hint: '有效订单总数',
+          icon: 'el-icon-s-order',
+          className: 'is-blue'
+        },
+        {
+          key: 'quantity',
+          label: '销量',
+          value: this.formatNumber(this.summary.totalQuantity),
+          hint: '商品销售件数',
+          icon: 'el-icon-shopping-bag-1',
+          className: 'is-orange'
+        },
+        {
+          key: 'avg',
+          label: '平均客单价',
+          value: `¥${this.formatMoney(this.summary.avgOrderValue)}`,
+          hint: '销售额 / 订单量',
+          icon: 'el-icon-s-marketing',
+          className: 'is-green'
+        }
+      ]
+    },
+    topChannels() {
+      return [...this.reportList]
+        .sort((a, b) => Number(b.salesAmount || 0) - Number(a.salesAmount || 0))
+        .slice(0, 5)
+    },
+    avgItemsPerOrder() {
+      return this.summary.orderCount > 0 ? this.summary.totalQuantity / this.summary.orderCount : 0
+    },
+    dateRangeLabel() {
+      if (this.currentQuery.startDate && this.currentQuery.endDate) {
+        return `${this.currentQuery.startDate} 至 ${this.currentQuery.endDate}`
+      }
+      return '等待筛选条件'
     }
   },
   created() {
@@ -152,41 +262,726 @@ export default {
 
 <style scoped>
 .shop-report-page {
-  background: #f5f7fa;
   min-height: calc(100vh - 84px);
+  padding: 24px;
+  background: #eef3fb;
+  color: #17243b;
 }
 
-.report-toolbar,
-.report-alert,
-.summary-row,
-.report-table {
+.report-shell {
+  display: grid;
+  grid-template-columns: minmax(0, 1fr) 300px;
+  gap: 22px;
+  max-width: 1680px;
+  margin: 0 auto;
+}
+
+.report-main,
+.report-side {
+  min-width: 0;
+}
+
+.hero-panel {
+  min-height: 150px;
+  padding: 26px 30px;
+  margin-bottom: 18px;
+  border-radius: 22px;
+  background: linear-gradient(110deg, #ffffff 0%, #f7f9ff 52%, #eaf0ff 100%);
+  box-shadow: 0 20px 45px rgba(42, 61, 115, 0.08);
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  overflow: hidden;
+}
+
+.eyebrow {
+  margin: 0 0 8px;
+  color: #6b6ff5;
+  font-size: 12px;
+  font-weight: 700;
+  text-transform: uppercase;
+}
+
+.hero-panel h2 {
+  margin: 0;
+  font-size: 28px;
+  line-height: 1.2;
+  color: #101b33;
+}
+
+.hero-desc {
+  max-width: 520px;
+  margin: 12px 0 0;
+  color: #7b8498;
+  line-height: 1.7;
+}
+
+.hero-visual {
+  position: relative;
+  width: 168px;
+  height: 110px;
+  border-radius: 24px;
+  background: #ffffff;
+  box-shadow: 0 18px 40px rgba(91, 105, 247, 0.18);
+}
+
+.hero-icon {
+  position: absolute;
+  top: 20px;
+  left: 22px;
+  width: 48px;
+  height: 48px;
+  border-radius: 16px;
+  background: #6c3df4;
+  color: #fff;
+  font-size: 25px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.hero-bars {
+  position: absolute;
+  right: 24px;
+  bottom: 22px;
+  display: flex;
+  align-items: flex-end;
+  gap: 9px;
+}
+
+.hero-bars i {
+  width: 10px;
+  border-radius: 999px;
+  background: #ffb24d;
+}
+
+.hero-bars i:nth-child(1) { height: 32px; }
+.hero-bars i:nth-child(2) { height: 52px; background: #5e6cf7; }
+.hero-bars i:nth-child(3) { height: 42px; }
+.hero-bars i:nth-child(4) { height: 64px; background: #ef4c43; }
+
+.report-toolbar {
   margin-bottom: 16px;
+  border-radius: 22px;
+  box-shadow: 0 16px 36px rgba(38, 51, 96, 0.06);
+  overflow: hidden;
+}
+
+.report-toolbar ::v-deep .order-date-filter {
+  padding: 16px 18px;
+  border: 0;
+  border-radius: 22px;
+  background: #fff;
+  display: grid;
+  grid-template-columns: auto 1fr;
+  align-items: center;
+  gap: 12px;
+}
+
+.report-toolbar ::v-deep .el-form-item {
+  height: 38px;
+  margin: 0;
+  padding: 3px;
+  border-radius: 15px;
+  background: #f6f8ff;
+  display: flex;
+  align-items: center;
+}
+
+.report-toolbar ::v-deep .el-form-item:nth-child(1) {
+  grid-column: 1 / -1;
+  justify-self: start;
+}
+
+.report-toolbar ::v-deep .el-form-item:nth-child(2) {
+  justify-self: start;
+}
+
+.report-toolbar ::v-deep .el-form-item__label {
+  height: 32px;
+  padding: 0 10px 0 8px;
+  color: #5d667a;
+  font-size: 13px;
+  font-weight: 700;
+  line-height: 32px;
+}
+
+.report-toolbar ::v-deep .el-form-item__content {
+  line-height: 32px;
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+
+.report-toolbar ::v-deep .date-action-row,
+.report-toolbar ::v-deep .filter-actions {
+  display: inline-flex;
+  align-items: center;
+  gap: 8px;
+  white-space: nowrap;
+}
+
+.report-toolbar ::v-deep .el-radio-group {
+  display: inline-flex;
+  gap: 4px;
+  padding: 0;
+  vertical-align: top;
+}
+
+.report-toolbar ::v-deep .el-radio-button__inner {
+  min-width: 56px;
+  height: 32px;
+  padding: 0 14px;
+  border: 0;
+  border-radius: 11px;
+  background: transparent;
+  color: #667086;
+  line-height: 32px;
+  box-shadow: none;
+  transition: all 0.18s ease;
+}
+
+.report-toolbar ::v-deep .el-radio-button:first-child .el-radio-button__inner,
+.report-toolbar ::v-deep .el-radio-button:last-child .el-radio-button__inner {
+  border-radius: 11px;
+}
+
+.report-toolbar ::v-deep .el-radio-button__orig-radio:checked + .el-radio-button__inner {
+  background: #6267f2;
+  color: #fff;
+  box-shadow: 0 8px 18px rgba(98, 103, 242, 0.22);
+}
+
+.report-toolbar ::v-deep .el-date-editor.el-input__inner {
+  width: 350px;
+  height: 32px;
+  padding: 0 12px;
+  border: 0;
+  border-radius: 12px;
+  background: #fff;
+  color: #29364f;
+  box-shadow: inset 0 0 0 1px rgba(98, 103, 242, 0.12);
+}
+
+.report-toolbar ::v-deep .el-date-editor .el-range-input {
+  width: 128px;
+  color: #29364f;
+  font-weight: 600;
+  background: transparent;
+}
+
+.report-toolbar ::v-deep .el-date-editor .el-range-separator {
+  width: 32px;
+  color: #8c94a8;
+  line-height: 30px;
+}
+
+.report-toolbar ::v-deep .el-date-editor .el-range__icon,
+.report-toolbar ::v-deep .el-date-editor .el-range__close-icon {
+  color: #9aa4bc;
+  line-height: 30px;
+}
+
+.report-toolbar ::v-deep .el-button {
+  height: 32px;
+  padding: 0 16px;
+  border-radius: 12px;
+  font-weight: 700;
+}
+
+.report-toolbar ::v-deep .el-button--primary {
+  background: #6267f2;
+  border-color: #6267f2;
+  box-shadow: 0 8px 18px rgba(98, 103, 242, 0.22);
+}
+
+.report-toolbar ::v-deep .el-button--default {
+  border-color: transparent;
+  background: #fff;
+  color: #6e778b;
+}
+
+.report-toolbar ::v-deep .el-button--warning.is-plain {
+  border-color: transparent;
+  background: #fff6e8;
+  color: #f39a20;
+}
+
+.report-toolbar ::v-deep .el-button + .el-button {
+  margin-left: 0;
+}
+
+.report-alert {
+  margin-bottom: 16px;
+  border: 0;
+  border-radius: 14px;
+}
+
+.summary-row {
+  margin-bottom: 18px;
+}
+
+.summary-row .el-col {
+  margin-bottom: 18px;
 }
 
 .summary-item {
-  min-height: 88px;
-  padding: 18px 20px;
+  position: relative;
+  min-height: 142px;
+  padding: 20px;
+  border: 0;
+  border-radius: 20px;
   background: #fff;
-  border: 1px solid #e5e6eb;
-  border-radius: 4px;
+  box-shadow: 0 16px 34px rgba(37, 50, 88, 0.08);
   display: flex;
   flex-direction: column;
+  justify-content: flex-end;
+  overflow: hidden;
+}
+
+.summary-item::after {
+  content: "";
+  position: absolute;
+  top: -38px;
+  right: -34px;
+  width: 112px;
+  height: 112px;
+  border-radius: 50%;
+  background: rgba(98, 103, 242, 0.1);
+}
+
+.summary-icon {
+  position: absolute;
+  top: 18px;
+  left: 20px;
+  width: 44px;
+  height: 44px;
+  border-radius: 15px;
+  background: #f1efff;
+  color: #6c3df4;
+  font-size: 22px;
+  display: flex;
+  align-items: center;
   justify-content: center;
 }
 
 .summary-label {
-  margin-bottom: 10px;
-  color: #606266;
+  margin-bottom: 8px;
+  color: #7b8498;
   font-size: 13px;
 }
 
 .summary-item strong {
-  color: #1f2d3d;
-  font-size: 24px;
+  color: #121d35;
+  font-size: 25px;
   line-height: 1.2;
 }
 
+.summary-item small {
+  margin-top: 10px;
+  color: #a0a8b8;
+}
+
+.summary-item.is-primary {
+  color: #fff;
+  background: linear-gradient(145deg, #7935f5 0%, #6028ee 100%);
+  box-shadow: 0 20px 36px rgba(99, 50, 236, 0.24);
+}
+
+.summary-item.is-primary .summary-label,
+.summary-item.is-primary small,
+.summary-item.is-primary strong {
+  color: #fff;
+}
+
+.summary-item.is-primary .summary-icon {
+  background: #fff;
+  color: #6c3df4;
+}
+
+.summary-item.is-blue .summary-icon {
+  background: #edf1ff;
+  color: #5e6cf7;
+}
+
+.summary-item.is-orange .summary-icon {
+  background: #fff2dd;
+  color: #f5a338;
+}
+
+.summary-item.is-green .summary-icon {
+  background: #eaf8f4;
+  color: #29b98f;
+}
+
+.table-card,
+.side-card {
+  border-radius: 22px;
+  background: #fff;
+  box-shadow: 0 18px 42px rgba(42, 61, 115, 0.08);
+}
+
+.table-card {
+  padding: 20px;
+}
+
+.section-heading {
+  margin-bottom: 18px;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 16px;
+}
+
+.section-heading h3 {
+  margin: 0;
+  color: #101b33;
+  font-size: 18px;
+}
+
+.section-heading p {
+  margin: 6px 0 0;
+  color: #8a93a6;
+  font-size: 12px;
+}
+
+.section-heading.compact {
+  margin-bottom: 14px;
+}
+
 .report-table {
   width: 100%;
+  border-radius: 14px;
+  overflow: hidden;
+}
+
+.report-table ::v-deep th.el-table__cell {
+  background: #f8f9fd;
+  color: #697287;
+  font-weight: 700;
+}
+
+.report-table ::v-deep .el-table__cell {
+  border-bottom-color: #eef1f7;
+}
+
+.report-table ::v-deep::before {
+  display: none;
+}
+
+.report-side {
+  display: flex;
+  flex-direction: column;
+  gap: 18px;
+}
+
+.side-card {
+  padding: 20px;
+}
+
+.performance-card {
+  overflow: hidden;
+}
+
+.gauge {
+  position: relative;
+  height: 142px;
+  margin: 4px 0 16px;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: flex-end;
+}
+
+.gauge-arc {
+  position: absolute;
+  top: 12px;
+  width: 156px;
+  height: 78px;
+  border: 24px solid #edf0f7;
+  border-bottom: 0;
+  border-radius: 156px 156px 0 0;
+}
+
+.gauge-arc::after {
+  content: "";
+  position: absolute;
+  inset: -24px -24px 0 -24px;
+  border: 24px solid #ffad55;
+  border-right-color: transparent;
+  border-bottom: 0;
+  border-radius: 156px 156px 0 0;
+  transform: rotate(10deg);
+  transform-origin: center bottom;
+}
+
+.gauge strong {
+  position: relative;
+  z-index: 1;
+  color: #101b33;
+  font-size: 28px;
+}
+
+.gauge span {
+  position: relative;
+  z-index: 1;
+  margin-top: 4px;
+  color: #8a93a6;
+  font-size: 12px;
+}
+
+.side-metric {
+  padding: 14px 0;
+  border-top: 1px solid #eef1f7;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+}
+
+.side-metric span {
+  color: #80899b;
+}
+
+.side-metric strong {
+  color: #101b33;
+  font-size: 18px;
+}
+
+.rank-list {
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+}
+
+.rank-item {
+  min-height: 58px;
+  padding: 10px 0;
+  border-bottom: 1px solid #f0f2f7;
+  display: grid;
+  grid-template-columns: 34px minmax(0, 1fr) auto;
+  gap: 10px;
+  align-items: center;
+}
+
+.rank-item:last-child {
+  border-bottom: 0;
+}
+
+.rank-index {
+  width: 28px;
+  height: 28px;
+  border-radius: 10px;
+  background: #f1efff;
+  color: #6c3df4;
+  font-weight: 700;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.rank-item strong,
+.rank-item small {
+  display: block;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.rank-item strong {
+  color: #1b2740;
+}
+
+.rank-item small {
+  margin-top: 4px;
+  color: #9aa3b4;
+}
+
+.rank-item em {
+  color: #1b2740;
+  font-style: normal;
+  font-weight: 700;
+}
+
+@media (max-width: 1280px) {
+  .report-shell {
+    grid-template-columns: 1fr;
+  }
+
+  .report-side {
+    display: grid;
+    grid-template-columns: repeat(2, minmax(0, 1fr));
+  }
+}
+
+@media (max-width: 768px) {
+  .shop-report-page {
+    padding: 14px;
+  }
+
+  .hero-panel {
+    padding: 22px;
+  }
+
+  .hero-visual {
+    display: none;
+  }
+
+  .report-side {
+    grid-template-columns: 1fr;
+  }
+
+  .report-toolbar ::v-deep .el-form-item {
+    width: 100%;
+    height: auto;
+    align-items: flex-start;
+    flex-direction: column;
+  }
+
+  .report-toolbar ::v-deep .el-form-item:nth-child(1),
+  .report-toolbar ::v-deep .el-form-item:nth-child(2) {
+    grid-column: 1;
+  }
+
+  .report-toolbar ::v-deep .date-action-row {
+    width: 100%;
+    flex-wrap: wrap;
+  }
+
+  .report-toolbar ::v-deep .el-date-editor.el-input__inner {
+    width: 100%;
+  }
+}
+</style>
+
+<style>
+.shop-report-date-popper {
+  width: 646px;
+  border: 0 !important;
+  border-radius: 18px !important;
+  background: #fff;
+  box-shadow: 0 24px 60px rgba(32, 45, 82, 0.16) !important;
+  overflow: hidden !important;
+}
+
+.shop-report-date-popper .popper__arrow {
+  display: none;
+}
+
+.shop-report-date-popper.el-picker-panel {
+  color: #24314a;
+}
+
+.shop-report-date-popper .el-picker-panel__body-wrapper {
+  border-radius: 18px !important;
+  overflow: hidden;
+}
+
+.shop-report-date-popper .el-picker-panel__body {
+  min-width: 646px;
+}
+
+.shop-report-date-popper .el-date-range-picker__content {
+  width: 50%;
+  padding: 20px 22px 18px;
+}
+
+.shop-report-date-popper .el-date-range-picker__content.is-left {
+  border-right-color: #eef1f7;
+}
+
+.shop-report-date-popper .el-date-range-picker__header {
+  height: 32px;
+  margin-bottom: 12px;
+  line-height: 32px;
+}
+
+.shop-report-date-popper .el-date-range-picker__header div {
+  color: #24314a;
+  font-size: 16px;
+  font-weight: 700;
+}
+
+.shop-report-date-popper .el-picker-panel__icon-btn {
+  color: #8c94a8;
+  font-size: 13px;
+  line-height: 32px;
+}
+
+.shop-report-date-popper .el-date-table {
+  table-layout: fixed;
+}
+
+.shop-report-date-popper .el-date-table th {
+  padding: 6px 0 10px;
+  color: #7d879b;
+  font-weight: 700;
+  border-bottom: 0;
+}
+
+.shop-report-date-popper .el-date-table td {
+  height: 36px;
+  padding: 2px 0;
+  color: #26364f;
+}
+
+.shop-report-date-popper .el-date-table td div {
+  height: 32px;
+  padding: 2px 0;
+  border-radius: 10px;
+  box-sizing: border-box;
+}
+
+.shop-report-date-popper .el-date-table td span {
+  width: 28px;
+  height: 28px;
+  border-radius: 9px;
+  line-height: 28px;
+}
+
+.shop-report-date-popper .el-date-table td.available:hover span {
+  background: #f0f2ff;
+  color: #6267f2;
+}
+
+.shop-report-date-popper .el-date-table td.in-range div,
+.shop-report-date-popper .el-date-table td.in-range div:hover {
+  background: #f4f6ff;
+}
+
+.shop-report-date-popper .el-date-table td.disabled div {
+  background: transparent;
+}
+
+.shop-report-date-popper .el-date-table td.disabled span {
+  color: #b7bfce;
+}
+
+.shop-report-date-popper .el-date-table td.prev-month span,
+.shop-report-date-popper .el-date-table td.next-month span {
+  color: #b7bfce;
+}
+
+.shop-report-date-popper .el-date-table td.start-date span,
+.shop-report-date-popper .el-date-table td.end-date span,
+.shop-report-date-popper .el-date-table td.current:not(.disabled) span {
+  background: #6267f2;
+  color: #fff;
+  box-shadow: 0 8px 16px rgba(98, 103, 242, 0.24);
+}
+
+.shop-report-date-popper .el-date-table td.start-date div,
+.shop-report-date-popper .el-date-table td.end-date div {
+  border-radius: 10px;
+}
+
+.shop-report-date-popper .el-date-table td.today span {
+  color: #6267f2;
+  font-weight: 700;
+}
+
+.shop-report-date-popper .el-date-table td.start-date.today span,
+.shop-report-date-popper .el-date-table td.end-date.today span,
+.shop-report-date-popper .el-date-table td.current.today span {
+  color: #fff;
 }
 </style>

+ 1 - 1
src/views/order/shopvalue/index.vue

@@ -1,5 +1,5 @@
 <template>
-  <div class="shop-value-analysis-page">
+  <div class="shop-value-analysis-page order-dashboard-page">
     <header class="page-header">
       <div>
         <h1 class="main-title">店铺价值分析</h1>

+ 115 - 12
src/views/supply/cost/index.vue

@@ -1,5 +1,5 @@
 <template>
-  <div class="app-container">
+  <div class="app-container supply-dashboard-page">
     <div class="page-header">
       <h2><i class="el-icon-s-order"></i> 供应商成本分析</h2>
       <p class="page-desc">分析各供应商针对特定产品的报价及成本竞争力</p>
@@ -47,20 +47,49 @@
         highlight-current-row
         style="width: 100%"
       >
-        <el-table-column prop="供应商名称" label="供应商名称" width="200" />
-        <el-table-column prop="参考价" label="参考价" width="120">
+        <el-table-column prop="供应商名称" label="供应商名称" min-width="220" />
+        <el-table-column prop="参考价" label="参考价" width="110" align="right">
           <template slot-scope="scope">
-            ¥{{ parseFloat(scope.row.参考价).toFixed(2) }}
+            ¥{{ formatNumber(scope.row.参考价, 2) }}
           </template>
         </el-table-column>
-        <el-table-column prop="成本排名" label="成本排名" width="120">
+        <el-table-column prop="入库数量" label="入库数量" width="120" align="right">
+          <template slot-scope="scope">
+            {{ formatNumber(scope.row.入库数量, 0) }}
+          </template>
+        </el-table-column>
+        <el-table-column prop="入库金额" label="入库金额" width="130" align="right">
+          <template slot-scope="scope">
+            ¥{{ formatNumber(scope.row.入库金额, 2) }}
+          </template>
+        </el-table-column>
+        <el-table-column prop="采购占比" label="采购占比" width="110" align="right">
+          <template slot-scope="scope">
+            {{ formatPercent(scope.row.采购占比) }}
+          </template>
+        </el-table-column>
+        <el-table-column prop="价格差异率" label="价格差异率" width="120" align="right">
+          <template slot-scope="scope">
+            {{ formatPercent(scope.row.价格差异率) }}
+          </template>
+        </el-table-column>
+        <el-table-column prop="成本排名" label="成本排名" width="100" align="center">
           <template slot-scope="scope"> 第{{ scope.row.成本排名 }}名 </template>
         </el-table-column>
-        <el-table-column prop="成本分数" label="成本分数" width="120">
+        <el-table-column prop="成本分数" label="成本分数" width="110" align="right">
+          <template slot-scope="scope">
+            {{ formatNumber(scope.row.成本分数, 2) }}
+          </template>
+        </el-table-column>
+        <el-table-column prop="成本等级" label="成本等级" width="100" align="center">
           <template slot-scope="scope">
-            {{ parseFloat(scope.row.成本分数).toFixed(2) }}
+            <el-tag :type="costLevelType(scope.row.成本等级)" size="mini">
+              {{ scope.row.成本等级 }}
+            </el-tag>
           </template>
         </el-table-column>
+        <el-table-column prop="入库行数" label="入库行数" width="100" align="right" />
+        <el-table-column prop="最近验收日期" label="最近验收日期" width="130" />
       </el-table>
 
       <div v-else class="no-data">
@@ -72,7 +101,7 @@
 </template>
 
 <script>
-import { download } from "@/utils";
+import { saveAs } from "file-saver";
 import { getProductDetails } from "@/api/supply";
 
 export default {
@@ -109,12 +138,12 @@ export default {
 
         // Flask API通常直接返回数据,没有code字段
         if (response && response.cost_details !== undefined) {
-          this.costDetails = response.cost_details || [];
+          this.costDetails = this.enrichCostDetails(response.cost_details || []);
           this.$modal.msgSuccess("成本分析完成");
         } else {
           // 尝试其他可能的数据结构
           if (response && Array.isArray(response)) {
-            this.costDetails = response || [];
+            this.costDetails = this.enrichCostDetails(response || []);
             this.$modal.msgSuccess("成本分析完成");
           } else {
             this.error = "获取成本数据失败,API响应格式不匹配";
@@ -130,6 +159,60 @@ export default {
       }
     },
 
+    enrichCostDetails(rows) {
+      const list = Array.isArray(rows) ? rows : [];
+      const totalAmount = list.reduce(
+        (sum, row) => sum + this.toNumber(row.入库金额),
+        0
+      );
+      const minPrice = list.reduce((min, row) => {
+        const price = this.toNumber(row.参考价);
+        return price > 0 ? Math.min(min, price) : min;
+      }, Number.POSITIVE_INFINITY);
+
+      return list.map((row) => {
+        const amount = this.toNumber(row.入库金额);
+        const price = this.toNumber(row.参考价);
+        const diffRate = Number.isFinite(minPrice) && minPrice > 0
+          ? (price - minPrice) / minPrice
+          : 0;
+        return {
+          ...row,
+          采购占比: totalAmount > 0 ? amount / totalAmount : 0,
+          价格差异率: diffRate,
+          成本等级: this.costLevel(row.成本排名),
+        };
+      });
+    },
+
+    toNumber(value) {
+      const number = Number(value);
+      return Number.isFinite(number) ? number : 0;
+    },
+
+    formatNumber(value, digits = 2) {
+      return this.toNumber(value).toLocaleString("zh-CN", {
+        minimumFractionDigits: digits,
+        maximumFractionDigits: digits,
+      });
+    },
+
+    formatPercent(value) {
+      return `${(this.toNumber(value) * 100).toFixed(1)}%`;
+    },
+
+    costLevel(rank) {
+      const value = Number(rank);
+      if (value === 1) return "低成本";
+      if (value <= 3) return "中成本";
+      return "高成本";
+    },
+
+    costLevelType(level) {
+      if (level === "低成本") return "success";
+      if (level === "中成本") return "warning";
+      return "danger";
+    },
     // 导出数据
     exportData() {
       if (!this.costDetails || this.costDetails.length === 0) {
@@ -138,15 +221,34 @@ export default {
       }
 
       // 创建CSV内容
-      const header = ["供应商名称", "参考价", "成本排名", "成本分数"].join(",");
+      const header = [
+        "供应商名称",
+        "参考价",
+        "入库数量",
+        "入库金额",
+        "采购占比",
+        "价格差异率",
+        "成本排名",
+        "成本分数",
+        "成本等级",
+        "入库行数",
+        "最近验收日期",
+      ].join(",");
       const csvContent = [
         header,
         ...this.costDetails.map((row) =>
           [
             `"${row.供应商名称}"`,
             row.参考价,
+            row.入库数量,
+            row.入库金额,
+            this.formatPercent(row.采购占比),
+            this.formatPercent(row.价格差异率),
             `第${row.成本排名}名`,
             row.成本分数,
+            row.成本等级,
+            row.入库行数,
+            row.最近验收日期,
           ].join(",")
         ),
       ].join("\n");
@@ -155,7 +257,7 @@ export default {
       const blob = new Blob(["\ufeff" + csvContent], {
         type: "text/csv;charset=utf-8;",
       });
-      download(blob, `供应商成本分析_${new Date().getTime()}.csv`);
+      saveAs(blob, `供应商成本分析_${new Date().getTime()}.csv`);
     },
   },
 };
@@ -211,3 +313,4 @@ export default {
   font-weight: bold;
 }
 </style>
+

+ 132 - 23
src/views/supply/delivery/index.vue

@@ -1,5 +1,5 @@
 <template>
-  <div class="app-container">
+  <div class="app-container supply-dashboard-page">
     <div class="page-header">
       <h2><i class="el-icon-s-opportunity"></i> 供应商交付分析</h2>
       <p class="page-desc">分析各供应商针对特定产品的交付表现</p>
@@ -47,33 +47,64 @@
         highlight-current-row
         style="width: 100%"
       >
-        <el-table-column prop="供应商名称" label="供应商名称" width="150" />
-        <el-table-column prop="交付分数" label="交付分数" width="120">
+        <el-table-column prop="供应商名称" label="供应商名称" min-width="220" />
+        <el-table-column prop="履约状态" label="履约状态" width="110" align="center">
           <template slot-scope="scope">
-            {{ parseFloat(scope.row.交付分数).toFixed(2) }}
+            <el-tag :type="statusType(scope.row.履约状态)" size="mini">
+              {{ scope.row.履约状态 }}
+            </el-tag>
           </template>
         </el-table-column>
-        <el-table-column prop="D1_准时率" label="准时率 (%)" width="120">
+        <el-table-column prop="风险等级" label="风险等级" width="100" align="center">
           <template slot-scope="scope">
-            {{ parseFloat(scope.row.D1_准时率).toFixed(2) }}%
+            <el-tag :type="riskType(scope.row.风险等级)" size="mini">
+              {{ scope.row.风险等级 }}
+            </el-tag>
           </template>
         </el-table-column>
-        <el-table-column prop="D2_平均偏差" label="平均偏差 (天)" width="130">
+        <el-table-column prop="交付分数" label="交付分数" width="110" align="right">
           <template slot-scope="scope">
-            {{ parseFloat(scope.row.D2_平均偏差).toFixed(2) }}
+            {{ formatNumber(scope.row.交付分数, 2) }}
           </template>
         </el-table-column>
-        <el-table-column prop="D3_最长延迟" label="最长延迟 (天)" width="130">
+        <el-table-column prop="D1_准时率" label="准时率" width="100" align="right">
           <template slot-scope="scope">
-            {{ parseFloat(scope.row.D3_最长延迟).toFixed(2) }}
+            {{ formatNumber(scope.row.D1_准时率, 2) }}%
           </template>
         </el-table-column>
-        <el-table-column prop="D4_满足率" label="满足率 (%)" width="120">
+        <el-table-column prop="D4_满足率" label="满足率" width="100" align="right">
           <template slot-scope="scope">
-            {{ parseFloat(scope.row.D4_满足率).toFixed(2) }}%
+            {{ formatNumber(scope.row.D4_满足率, 2) }}%
           </template>
         </el-table-column>
-      </el-table>
+        <el-table-column prop="计划数量" label="计划数量" width="120" align="right">
+          <template slot-scope="scope">
+            {{ formatNumber(scope.row.计划数量, 0) }}
+          </template>
+        </el-table-column>
+        <el-table-column prop="实际入库数量" label="实际入库数量" width="130" align="right">
+          <template slot-scope="scope">
+            {{ formatNumber(scope.row.实际入库数量, 0) }}
+          </template>
+        </el-table-column>
+        <el-table-column prop="未满足数量" label="未满足数量" width="120" align="right">
+          <template slot-scope="scope">
+            {{ formatNumber(scope.row.未满足数量, 0) }}
+          </template>
+        </el-table-column>
+        <el-table-column prop="匹配行数" label="匹配行数" width="100" align="right" />
+        <el-table-column prop="准时行数" label="准时行数" width="100" align="right" />
+        <el-table-column prop="延迟行数" label="延迟行数" width="100" align="right" />
+        <el-table-column prop="D2_平均偏差" label="平均偏差(天)" width="130" align="right">
+          <template slot-scope="scope">
+            {{ formatNumber(scope.row.D2_平均偏差, 2) }}
+          </template>
+        </el-table-column>
+        <el-table-column prop="D3_最长延迟" label="最长延迟(天)" width="130" align="right">
+          <template slot-scope="scope">
+            {{ formatNumber(scope.row.D3_最长延迟, 2) }}
+          </template>
+        </el-table-column>      </el-table>
 
       <div v-else class="no-data">
         <p v-if="error" class="error">{{ error }}</p>
@@ -84,7 +115,7 @@
 </template>
 
 <script>
-import { download } from "@/utils";
+import { saveAs } from "file-saver";
 import { getProductDetails } from "@/api/supply";
 
 export default {
@@ -121,12 +152,12 @@ export default {
 
         // Flask API通常直接返回数据,没有code字段
         if (response && response.delivery_details !== undefined) {
-          this.deliveryDetails = response.delivery_details || [];
+          this.deliveryDetails = this.enrichDeliveryDetails(response.delivery_details || []);
           this.$modal.msgSuccess("交付分析完成");
         } else {
           // 尝试其他可能的数据结构
           if (response && Array.isArray(response)) {
-            this.deliveryDetails = response || [];
+            this.deliveryDetails = this.enrichDeliveryDetails(response || []);
             this.$modal.msgSuccess("交付分析完成");
           } else {
             this.error = "获取交付数据失败,API响应格式不匹配";
@@ -142,6 +173,68 @@ export default {
       }
     },
 
+    enrichDeliveryDetails(rows) {
+      const list = Array.isArray(rows) ? rows : [];
+      return list.map((row) => {
+        const planQty = this.firstNumber(row.计划数量, row.planQty);
+        const receiptQty = this.firstNumber(row.实际入库数量, row.receiptQty);
+        const onTimeRate = this.firstNumber(row.D1_准时率, row.准时率, row.onTimeRate);
+        const fulfillmentRate = this.firstNumber(row.D4_满足率, row.满足率, row.fulfillmentRate);
+        const maxDelayDays = this.firstNumber(row.D3_最长延迟, row.最长延迟, row.maxDelayDays);
+        const shortageQty = Math.max(0, planQty - receiptQty);
+        return {
+          ...row,
+          计划数量: planQty,
+          实际入库数量: receiptQty,
+          未满足数量: this.firstNumber(row.未满足数量, row.shortageQty, shortageQty),
+          匹配行数: this.firstNumber(row.匹配行数, row.matchedLines),
+          准时行数: this.firstNumber(row.准时行数, row.onTimeLines),
+          延迟行数: this.firstNumber(row.延迟行数, row.delayedLines),
+          履约状态: row.履约状态 || row.deliveryStatus || this.deliveryStatus(onTimeRate, fulfillmentRate, maxDelayDays),
+          风险等级: row.风险等级 || row.riskLevel || this.riskLevel(onTimeRate, fulfillmentRate, maxDelayDays),
+        };
+      });
+    },
+
+    firstNumber(...values) {
+      for (const value of values) {
+        const number = Number(value);
+        if (Number.isFinite(number)) return number;
+      }
+      return 0;
+    },
+
+    formatNumber(value, digits = 2) {
+      return this.firstNumber(value).toLocaleString("zh-CN", {
+        minimumFractionDigits: digits,
+        maximumFractionDigits: digits,
+      });
+    },
+
+    deliveryStatus(onTimeRate, fulfillmentRate, maxDelayDays) {
+      if (fulfillmentRate < 80 || maxDelayDays >= 30) return "严重异常";
+      if (fulfillmentRate < 95) return "数量不足";
+      if (onTimeRate < 80 || maxDelayDays > 0) return "存在延迟";
+      return "正常";
+    },
+
+    riskLevel(onTimeRate, fulfillmentRate, maxDelayDays) {
+      if (fulfillmentRate < 80 || onTimeRate < 60 || maxDelayDays >= 30) return "高";
+      if (fulfillmentRate < 95 || onTimeRate < 90 || maxDelayDays > 0) return "中";
+      return "低";
+    },
+
+    statusType(status) {
+      if (status === "正常") return "success";
+      if (status === "存在延迟" || status === "数量不足") return "warning";
+      return "danger";
+    },
+
+    riskType(level) {
+      if (level === "低") return "success";
+      if (level === "中") return "warning";
+      return "danger";
+    },
     // 导出数据
     exportData() {
       if (!this.deliveryDetails || this.deliveryDetails.length === 0) {
@@ -152,22 +245,38 @@ export default {
       // 创建CSV内容
       const header = [
         "供应商名称",
+        "履约状态",
+        "风险等级",
         "交付分数",
         "准时率(%)",
+        "满足率(%)",
+        "计划数量",
+        "实际入库数量",
+        "未满足数量",
+        "匹配行数",
+        "准时行数",
+        "延迟行数",
         "平均偏差(天)",
         "最长延迟(天)",
-        "满足率(%)",
       ].join(",");
       const csvContent = [
         header,
         ...this.deliveryDetails.map((row) =>
           [
             `"${row.供应商名称}"`,
-            parseFloat(row.交付分数).toFixed(2),
-            parseFloat(row.D1_准时率).toFixed(2),
-            parseFloat(row.D2_平均偏差).toFixed(2),
-            parseFloat(row.D3_最长延迟).toFixed(2),
-            parseFloat(row.D4_满足率).toFixed(2),
+            row.履约状态,
+            row.风险等级,
+            this.formatNumber(row.交付分数, 2),
+            this.formatNumber(row.D1_准时率, 2),
+            this.formatNumber(row.D4_满足率, 2),
+            row.计划数量,
+            row.实际入库数量,
+            row.未满足数量,
+            row.匹配行数,
+            row.准时行数,
+            row.延迟行数,
+            this.formatNumber(row.D2_平均偏差, 2),
+            this.formatNumber(row.D3_最长延迟, 2),
           ].join(",")
         ),
       ].join("\n");
@@ -176,7 +285,7 @@ export default {
       const blob = new Blob(["\ufeff" + csvContent], {
         type: "text/csv;charset=utf-8;",
       });
-      download(blob, `供应商交付分析_${new Date().getTime()}.csv`);
+      saveAs(blob, `供应商交付分析_${new Date().getTime()}.csv`);
     },
   },
 };

+ 317 - 292
src/views/supply/monitor/index.vue

@@ -1,17 +1,40 @@
 <template>
-  <div class="app-container">
+  <div class="app-container supply-dashboard-page">
     <div class="page-header">
-      <h2><i class="el-icon-s-data"></i> 供应商查询、对比与付款计划</h2>
-      <p class="page-desc">基于供应商账期、采购订单、采购入库和订单入库合并数据,补充供应商实时查询、对比分析和预计付款计划。</p>
+      <h2><i class="el-icon-s-data"></i> 供应监测</h2>
+      <p class="page-desc">统一查看供应商评分卡、供应商筛选对比、月度来货对账、付款计划与付款建议。</p>
     </div>
 
-    <el-card class="box-card">
+    <div class="metric-grid">
+      <div class="metric-card primary">
+        <span>供应商总数</span>
+        <strong>{{ supplierList.length }}</strong>
+        <em>当前查询范围</em>
+      </div>
+      <div class="metric-card success">
+        <span>A级供应商</span>
+        <strong>{{ levelCount('A') }}</strong>
+        <em>综合分 ≥ 85</em>
+      </div>
+      <div class="metric-card warning">
+        <span>预警供应商</span>
+        <strong>{{ warningSuppliers.length }}</strong>
+        <em>低分/交付/完成率异常</em>
+      </div>
+      <div class="metric-card danger">
+        <span>到期付款金额</span>
+        <strong>¥{{ compactMoney(overduePayAmount) }}</strong>
+        <em>已到期计划</em>
+      </div>
+    </div>
+
+    <el-card class="box-card filter-card">
       <div slot="header" class="clearfix">
-        <span><i class="el-icon-search"></i> 查询条件</span>
+        <span><i class="el-icon-search"></i> 查询与筛选</span>
       </div>
       <el-form :inline="true" size="small" class="form-inline">
         <el-form-item label="供应商名称">
-          <el-input v-model="query.supplierName" clearable placeholder="输入供应商名称" style="width: 260px" @keyup.enter.native="handleQuery" />
+          <el-input v-model="query.supplierName" clearable placeholder="输入供应商名称" style="width: 240px" @keyup.enter.native="handleQuery" />
         </el-form-item>
         <el-form-item label="日期范围">
           <el-date-picker
@@ -23,63 +46,104 @@
             end-placeholder="结束日期"
           />
         </el-form-item>
+        <el-form-item label="供应商等级">
+          <el-select v-model="localFilters.supplierLevel" clearable placeholder="全部等级" style="width: 130px">
+            <el-option label="A级" value="A" />
+            <el-option label="B级" value="B" />
+            <el-option label="C级" value="C" />
+            <el-option label="预警" value="预警" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="预警状态">
+          <el-select v-model="localFilters.warningTag" clearable placeholder="全部状态" style="width: 150px">
+            <el-option label="正常" value="正常" />
+            <el-option label="综合分预警" value="综合分预警" />
+            <el-option label="质量预警" value="质量预警" />
+            <el-option label="交付预警" value="交付预警" />
+            <el-option label="完成率预警" value="完成率预警" />
+          </el-select>
+        </el-form-item>
         <el-form-item>
           <el-button type="primary" icon="el-icon-search" @click="handleQuery">查询</el-button>
           <el-button icon="el-icon-refresh" @click="resetQuery">重置</el-button>
         </el-form-item>
       </el-form>
-    </el-card>
 
-    <el-alert
-      class="box-card"
-      title="付款计划当前按 实际验收日期 + 供应商账期 推算;如果后续接入发票日期和真实付款日期,可以替换为真实付款计划。"
-      type="warning"
-      show-icon
-      :closable="false"
-    />
+      <div class="filter-divider" />
+      <div class="filter-title">
+        <span>多维筛选</span>
+        <em>当前显示 {{ filteredSupplierList.length }} / {{ supplierList.length }} 个供应商</em>
+      </div>
+      <el-form :inline="true" size="small" class="form-inline advanced-filter">
+        <el-form-item label="综合分">
+          <div class="range-input">
+            <el-input-number v-model="localFilters.minTotalScore" :min="0" :max="100" :controls="false" placeholder="最小" />
+            <span>至</span>
+            <el-input-number v-model="localFilters.maxTotalScore" :min="0" :max="100" :controls="false" placeholder="最大" />
+          </div>
+        </el-form-item>
+        <el-form-item label="账期">
+          <div class="range-input">
+            <el-input-number v-model="localFilters.minTermDays" :min="0" :controls="false" placeholder="最小" />
+            <span>至</span>
+            <el-input-number v-model="localFilters.maxTermDays" :min="0" :controls="false" placeholder="最大" />
+            <span>天</span>
+          </div>
+        </el-form-item>
+        <el-form-item label="入库金额">
+          <div class="range-input wide">
+            <el-input-number v-model="localFilters.minReceiptAmount" :min="0" :controls="false" placeholder="最小金额" />
+            <span>至</span>
+            <el-input-number v-model="localFilters.maxReceiptAmount" :min="0" :controls="false" placeholder="最大金额" />
+          </div>
+        </el-form-item>
+        <el-form-item label="交付及时率">
+          <div class="range-input">
+            <el-input-number v-model="localFilters.minDeliveryRate" :min="0" :max="100" :controls="false" placeholder="最小" />
+            <span>至</span>
+            <el-input-number v-model="localFilters.maxDeliveryRate" :min="0" :max="100" :controls="false" placeholder="最大" />
+            <span>%</span>
+          </div>
+        </el-form-item>
+        <el-form-item label="完成率">
+          <div class="range-input">
+            <el-input-number v-model="localFilters.minCompletionRate" :min="0" :max="100" :controls="false" placeholder="最小" />
+            <span>至</span>
+            <el-input-number v-model="localFilters.maxCompletionRate" :min="0" :max="100" :controls="false" placeholder="最大" />
+            <span>%</span>
+          </div>
+        </el-form-item>
+        <el-form-item>
+          <el-button icon="el-icon-refresh-left" @click="resetLocalFilters">清空筛选</el-button>
+        </el-form-item>
+      </el-form>
+    </el-card>
 
     <el-tabs v-model="activeTab" type="border-card" class="box-card" @tab-click="handleTabClick">
-      <el-tab-pane label="供应商列表" name="list">
-        <el-table
-          v-loading="loading"
-          :data="pagedSupplierList"
-          row-key="supplierName"
-          border
-          highlight-current-row
-          style="width: 100%"
-          @selection-change="handleSelectionChange"
-        >
+      <el-tab-pane label="评分卡" name="scorecard">
+        <el-table v-loading="loading" :data="pagedSupplierList" row-key="supplierName" border highlight-current-row style="width: 100%" @selection-change="handleSelectionChange">
           <el-table-column type="selection" width="48" reserve-selection />
-          <el-table-column prop="supplierName" label="供应商名称" min-width="220" fixed="left" show-overflow-tooltip />
-          <el-table-column prop="supplierCode" label="供应商代码" width="110" />
-          <el-table-column label="账期" width="90" align="right">
-            <template slot-scope="scope">{{ scope.row.termDays || 0 }}天</template>
-          </el-table-column>
-          <el-table-column label="订单量" prop="orderCount" width="90" align="right" />
-          <el-table-column label="订单数量" width="110" align="right">
-            <template slot-scope="scope">{{ formatNumber(scope.row.orderQty) }}</template>
+          <el-table-column prop="supplierName" label="供应商名称" min-width="230" fixed="left" show-overflow-tooltip />
+          <el-table-column prop="supplierLevel" label="等级" width="90" align="center">
+            <template slot-scope="scope"><el-tag :type="levelTag(scope.row.supplierLevel)" size="mini">{{ scope.row.supplierLevel }}</el-tag></template>
           </el-table-column>
-          <el-table-column label="入库数量" width="110" align="right">
-            <template slot-scope="scope">{{ formatNumber(scope.row.receiptQty) }}</template>
+          <el-table-column prop="warningTag" label="预警" width="120" align="center">
+            <template slot-scope="scope"><el-tag :type="warningTagType(scope.row.warningTag)" size="mini">{{ scope.row.warningTag }}</el-tag></template>
           </el-table-column>
-          <el-table-column label="入库金额" width="130" align="right">
-            <template slot-scope="scope">¥{{ formatMoney(scope.row.receiptAmount) }}</template>
-          </el-table-column>
-          <el-table-column label="交付及时率" width="120" align="right">
-            <template slot-scope="scope">{{ formatPercent(scope.row.deliveryRate) }}</template>
-          </el-table-column>
-          <el-table-column label="完成率" width="110" align="right">
-            <template slot-scope="scope">{{ formatPercent(scope.row.completionRate) }}</template>
+          <el-table-column label="综合分" width="110" align="right">
+            <template slot-scope="scope"><strong>{{ formatNumber(scope.row.totalScore) }}</strong></template>
           </el-table-column>
+          <el-table-column label="成本分" width="100" align="right"><template slot-scope="scope">{{ formatNumber(scope.row.costScore) }}</template></el-table-column>
+          <el-table-column label="交付分" width="100" align="right"><template slot-scope="scope">{{ formatNumber(scope.row.deliveryScore) }}</template></el-table-column>
+          <el-table-column label="账期分" width="100" align="right"><template slot-scope="scope">{{ formatNumber(scope.row.termScore) }}</template></el-table-column>
+          <el-table-column label="QA合格率" width="120" align="right"><template slot-scope="scope">{{ scope.row.qaPassRate === null || scope.row.qaPassRate === undefined ? '暂无' : formatPercent(scope.row.qaPassRate) }}</template></el-table-column>
+          <el-table-column label="不合格数" width="110" align="right"><template slot-scope="scope">{{ formatNumber(scope.row.qaDefectQty, 0) }}</template></el-table-column>
+          <el-table-column label="账期" width="90" align="right"><template slot-scope="scope">{{ scope.row.termDays || 0 }}天</template></el-table-column>
+          <el-table-column label="入库金额" width="130" align="right"><template slot-scope="scope">¥{{ formatMoney(scope.row.receiptAmount) }}</template></el-table-column>
+          <el-table-column label="交付及时率" width="120" align="right"><template slot-scope="scope">{{ formatPercent(scope.row.deliveryRate) }}</template></el-table-column>
+          <el-table-column label="完成率" width="110" align="right"><template slot-scope="scope">{{ formatPercent(scope.row.completionRate) }}</template></el-table-column>
         </el-table>
-        <div class="pagination-wrap">
-          <el-pagination
-            :current-page.sync="pagination.list.page"
-            :page-size="pagination.list.size"
-            :total="supplierList.length"
-            layout="total, prev, pager, next, jumper"
-          />
-        </div>
+        <div class="pagination-wrap"><el-pagination :current-page.sync="pagination.list.page" :page-size="pagination.list.size" :total="filteredSupplierList.length" layout="total, prev, pager, next, jumper" /></div>
       </el-tab-pane>
 
       <el-tab-pane label="供应商对比" name="compare">
@@ -92,66 +156,59 @@
           <div v-if="compareList.length === 0" class="chart-empty">请选择 2-5 个供应商并生成对比</div>
           <div ref="compareChart" class="compare-chart" />
         </div>
-        <el-table v-loading="compareLoading" :data="pagedCompareList" border highlight-current-row style="width: 100%">
+        <el-table v-loading="compareLoading" :data="compareList" empty-text="暂无对比数据" border highlight-current-row style="width: 100%">
           <el-table-column prop="supplierName" label="供应商名称" min-width="220" fixed="left" show-overflow-tooltip />
-          <el-table-column label="账期" width="90" align="right">
-            <template slot-scope="scope">{{ scope.row.termDays || 0 }}天</template>
-          </el-table-column>
-          <el-table-column label="订单量" prop="orderCount" width="90" align="right" />
-          <el-table-column label="订单金额" width="130" align="right">
-            <template slot-scope="scope">¥{{ formatMoney(scope.row.orderAmount) }}</template>
-          </el-table-column>
-          <el-table-column label="入库金额" width="130" align="right">
-            <template slot-scope="scope">¥{{ formatMoney(scope.row.receiptAmount) }}</template>
-          </el-table-column>
-          <el-table-column label="交付及时率" width="120" align="right">
-            <template slot-scope="scope">{{ formatPercent(scope.row.deliveryRate) }}</template>
-          </el-table-column>
-          <el-table-column label="完成率" width="110" align="right">
-            <template slot-scope="scope">{{ formatPercent(scope.row.completionRate) }}</template>
-          </el-table-column>
-          <el-table-column label="未完工数" width="120" align="right">
-            <template slot-scope="scope">{{ formatNumber(scope.row.uncompletedQty) }}</template>
-          </el-table-column>
+          <el-table-column label="账期" width="90" align="right"><template slot-scope="scope">{{ scope.row.termDays || 0 }}天</template></el-table-column>
+          <el-table-column label="订单金额" width="130" align="right"><template slot-scope="scope">¥{{ formatMoney(scope.row.orderAmount) }}</template></el-table-column>
+          <el-table-column label="入库金额" width="130" align="right"><template slot-scope="scope">¥{{ formatMoney(scope.row.receiptAmount) }}</template></el-table-column>
+          <el-table-column label="交付及时率" width="120" align="right"><template slot-scope="scope">{{ formatPercent(scope.row.deliveryRate) }}</template></el-table-column>
+          <el-table-column label="完成率" width="110" align="right"><template slot-scope="scope">{{ formatPercent(scope.row.completionRate) }}</template></el-table-column>
+          <el-table-column label="未完工数" width="120" align="right"><template slot-scope="scope">{{ formatNumber(scope.row.uncompletedQty) }}</template></el-table-column>
+        </el-table>
+      </el-tab-pane>
+
+      <el-tab-pane label="预警列表" name="warnings">
+        <el-table :data="warningSuppliers" border highlight-current-row style="width: 100%">
+          <el-table-column prop="supplierName" label="供应商名称" min-width="230" show-overflow-tooltip />
+          <el-table-column prop="warningTag" label="预警类型" width="130" align="center"><template slot-scope="scope"><el-tag :type="warningTagType(scope.row.warningTag)" size="mini">{{ scope.row.warningTag }}</el-tag></template></el-table-column>
+          <el-table-column label="综合分" width="110" align="right"><template slot-scope="scope">{{ formatNumber(scope.row.totalScore) }}</template></el-table-column>
+          <el-table-column label="交付及时率" width="120" align="right"><template slot-scope="scope">{{ formatPercent(scope.row.deliveryRate) }}</template></el-table-column>
+          <el-table-column label="完成率" width="110" align="right"><template slot-scope="scope">{{ formatPercent(scope.row.completionRate) }}</template></el-table-column>
+          <el-table-column label="建议动作" min-width="220"><template slot-scope="scope">{{ warningSuggestion(scope.row) }}</template></el-table-column>
+        </el-table>
+      </el-tab-pane>
+
+      <el-tab-pane label="月度来货对账" name="reconciliation">
+        <el-table v-loading="reconciliationLoading" :data="reconciliationList" border highlight-current-row style="width: 100%">
+          <el-table-column prop="month" label="月份" width="110" fixed="left" />
+          <el-table-column prop="supplierName" label="供应商名称" min-width="230" show-overflow-tooltip />
+          <el-table-column label="入库单数" prop="receiptCount" width="100" align="right" />
+          <el-table-column label="入库行数" prop="receiptLines" width="100" align="right" />
+          <el-table-column label="入库数量" width="120" align="right"><template slot-scope="scope">{{ formatNumber(scope.row.receiptQty) }}</template></el-table-column>
+          <el-table-column label="入库金额" width="140" align="right"><template slot-scope="scope">¥{{ formatMoney(scope.row.receiptAmount) }}</template></el-table-column>
+          <el-table-column label="平均入库价" width="130" align="right"><template slot-scope="scope">¥{{ formatMoney(scope.row.avgReceiptPrice) }}</template></el-table-column>
         </el-table>
-        <div class="pagination-wrap">
-          <el-pagination
-            :current-page.sync="pagination.compare.page"
-            :page-size="pagination.compare.size"
-            :total="compareList.length"
-            layout="total, prev, pager, next, jumper"
-          />
-        </div>
       </el-tab-pane>
 
       <el-tab-pane label="付款计划" name="payment">
+        <div class="summary-strip">
+          <div v-for="item in paymentSummary" :key="item.month" class="summary-item">
+            <span>{{ item.month }}</span>
+            <strong>¥{{ compactMoney(item.estimatedPayAmount) }}</strong>
+            <em>{{ item.suggestion }}</em>
+          </div>
+        </div>
         <el-table v-loading="paymentLoading" :data="pagedPaymentPlan" border highlight-current-row style="width: 100%">
           <el-table-column prop="dueDate" label="预计付款日" width="120" fixed="left" />
           <el-table-column prop="supplierName" label="供应商名称" min-width="220" show-overflow-tooltip />
-          <el-table-column label="账期" width="90" align="right">
-            <template slot-scope="scope">{{ scope.row.termDays || 0 }}天</template>
-          </el-table-column>
-          <el-table-column label="预计付款金额" width="150" align="right">
-            <template slot-scope="scope">¥{{ formatMoney(scope.row.estimatedPayAmount) }}</template>
-          </el-table-column>
-          <el-table-column label="入库数量" width="120" align="right">
-            <template slot-scope="scope">{{ formatNumber(scope.row.receiptQty) }}</template>
-          </el-table-column>
+          <el-table-column label="账期" width="90" align="right"><template slot-scope="scope">{{ scope.row.termDays || 0 }}天</template></el-table-column>
+          <el-table-column label="预计付款金额" width="150" align="right"><template slot-scope="scope">¥{{ formatMoney(scope.row.estimatedPayAmount) }}</template></el-table-column>
+          <el-table-column label="入库数量" width="120" align="right"><template slot-scope="scope">{{ formatNumber(scope.row.receiptQty) }}</template></el-table-column>
           <el-table-column prop="receiptLines" label="入库行数" width="100" align="right" />
-          <el-table-column prop="status" label="状态" width="110" align="center">
-            <template slot-scope="scope">
-              <el-tag :type="paymentTag(scope.row.status)" size="mini">{{ scope.row.status }}</el-tag>
-            </template>
-          </el-table-column>
+          <el-table-column prop="status" label="状态" width="110" align="center"><template slot-scope="scope"><el-tag :type="paymentTag(scope.row.status)" size="mini">{{ scope.row.status }}</el-tag></template></el-table-column>
+          <el-table-column label="建议" width="120" align="center"><template slot-scope="scope"><el-tag :type="paySuggestionType(scope.row.status)" size="mini">{{ paySuggestion(scope.row.status) }}</el-tag></template></el-table-column>
         </el-table>
-        <div class="pagination-wrap">
-          <el-pagination
-            :current-page.sync="pagination.payment.page"
-            :page-size="pagination.payment.size"
-            :total="paymentPlan.length"
-            layout="total, prev, pager, next, jumper"
-          />
-        </div>
+        <div class="pagination-wrap"><el-pagination :current-page.sync="pagination.payment.page" :page-size="pagination.payment.size" :total="paymentPlan.length" layout="total, prev, pager, next, jumper" /></div>
       </el-tab-pane>
     </el-tabs>
   </div>
@@ -159,95 +216,72 @@
 
 <script>
 import * as echarts from 'echarts'
-import { getSupplyMonitorCompare, getSupplyMonitorSuppliers, getSupplyPaymentPlan } from '@/api/supply'
+import { getSupplyMonitorCompare, getSupplyMonitorSuppliers, getSupplyPaymentPlan, getSupplyPaymentSummary, getSupplyReceiptReconciliation } from '@/api/supply'
 
 export default {
   name: 'SupplyMonitorEnhancement',
   data() {
     return {
-      activeTab: 'list',
+      activeTab: 'scorecard',
       loading: false,
       compareLoading: false,
       paymentLoading: false,
+      reconciliationLoading: false,
       compareChart: null,
-      query: {
-        supplierName: ''
-      },
+      query: { supplierName: '' },
+      localFilters: this.createEmptyLocalFilters(),
       dateRange: [],
       supplierList: [],
       selectedSuppliers: [],
       compareList: [],
       paymentPlan: [],
+      paymentSummary: [],
+      reconciliationList: [],
       pagination: {
         list: { page: 1, size: 20 },
-        compare: { page: 1, size: 20 },
         payment: { page: 1, size: 20 }
       }
     }
   },
-  created() {
-    this.handleQuery()
-  },
-  mounted() {
-    window.addEventListener('resize', this.handleResize)
+  computed: {
+    filteredSupplierList() { return this.supplierList.filter(item => this.matchLocalFilters(item)) },
+    pagedSupplierList() { return this.slicePage(this.filteredSupplierList, this.pagination.list) },
+    pagedPaymentPlan() { return this.slicePage(this.paymentPlan, this.pagination.payment) },
+    warningSuppliers() { return this.supplierList.filter(item => item.warningTag && item.warningTag !== '正常') },
+    overduePayAmount() { return this.paymentPlan.filter(item => item.status === '已到期').reduce((sum, item) => sum + Number(item.estimatedPayAmount || 0), 0) }
   },
+  created() { this.handleQuery() },
+  mounted() { window.addEventListener('resize', this.handleResize) },
   beforeDestroy() {
     window.removeEventListener('resize', this.handleResize)
-    if (this.compareChart) {
-      this.compareChart.dispose()
-      this.compareChart = null
-    }
-  },
-  computed: {
-    pagedSupplierList() {
-      return this.slicePage(this.supplierList, this.pagination.list)
-    },
-    pagedCompareList() {
-      return this.slicePage(this.compareList, this.pagination.compare)
-    },
-    pagedPaymentPlan() {
-      return this.slicePage(this.paymentPlan, this.pagination.payment)
-    }
+    if (this.compareChart) this.compareChart.dispose()
   },
   methods: {
-    slicePage(list, pager) {
-      const start = (pager.page - 1) * pager.size
-      return list.slice(start, start + pager.size)
+    createEmptyLocalFilters() {
+      return {
+        supplierLevel: '', warningTag: '', minTotalScore: undefined, maxTotalScore: undefined,
+        minTermDays: undefined, maxTermDays: undefined, minReceiptAmount: undefined, maxReceiptAmount: undefined,
+        minDeliveryRate: undefined, maxDeliveryRate: undefined, minCompletionRate: undefined, maxCompletionRate: undefined
+      }
     },
+    slicePage(list, pager) { return list.slice((pager.page - 1) * pager.size, (pager.page - 1) * pager.size + pager.size) },
     buildParams() {
-      const params = {
-        supplierName: this.query.supplierName
-      }
-      if (this.dateRange && this.dateRange.length === 2) {
-        params.startDate = this.dateRange[0]
-        params.endDate = this.dateRange[1]
-      }
+      const params = { supplierName: this.query.supplierName }
+      if (this.dateRange && this.dateRange.length === 2) { params.startDate = this.dateRange[0]; params.endDate = this.dateRange[1] }
       return params
     },
-    handleQuery() {
-      this.loadSuppliers()
-      if (this.activeTab === 'payment') {
-        this.loadPaymentPlan()
-      } else {
-        this.paymentPlan = []
-      }
+    async handleQuery() {
+      await this.loadSuppliers()
       this.compareList = []
-      this.$nextTick(this.renderCompareChart)
-    },
-    resetQuery() {
-      this.query.supplierName = ''
-      this.dateRange = []
-      this.handleQuery()
+      this.loadPaymentPlan()
+      this.loadPaymentSummary()
+      this.loadReconciliation()
     },
+    resetQuery() { this.query.supplierName = ''; this.dateRange = []; this.resetLocalFilters(false); this.handleQuery() },
+    resetLocalFilters(resetPage = true) { this.localFilters = this.createEmptyLocalFilters(); if (resetPage) this.pagination.list.page = 1 },
     async loadSuppliers() {
       this.loading = true
-      try {
-        const res = await getSupplyMonitorSuppliers(this.buildParams())
-        this.supplierList = Array.isArray(res.data) ? res.data : []
-        this.pagination.list.page = 1
-      } finally {
-        this.loading = false
-      }
+      try { const res = await getSupplyMonitorSuppliers(this.buildParams()); this.supplierList = Array.isArray(res.data) ? res.data : []; this.pagination.list.page = 1 } finally { this.loading = false }
     },
     async loadPaymentPlan() {
       this.paymentLoading = true
@@ -255,175 +289,166 @@ export default {
         const res = await getSupplyPaymentPlan(this.buildParams())
         this.paymentPlan = Array.isArray(res.data) ? res.data : []
         this.pagination.payment.page = 1
-      } finally {
-        this.paymentLoading = false
-      }
+      } catch (e) {
+        this.paymentPlan = []
+      } finally { this.paymentLoading = false }
     },
-    handleTabClick(tab) {
-      if (tab.name === 'payment' && this.paymentPlan.length === 0) {
-        this.loadPaymentPlan()
-      }
-      if (tab.name === 'compare') {
-        this.$nextTick(this.renderCompareChart)
+    async loadPaymentSummary() {
+      try {
+        const res = await getSupplyPaymentSummary(this.buildParams())
+        this.paymentSummary = Array.isArray(res.data) ? res.data : []
+      } catch (e) {
+        this.paymentSummary = []
       }
     },
+    async loadReconciliation() {
+      this.reconciliationLoading = true
+      try {
+        const res = await getSupplyReceiptReconciliation(this.buildParams())
+        this.reconciliationList = Array.isArray(res.data) ? res.data : []
+      } catch (e) {
+        this.reconciliationList = []
+      } finally { this.reconciliationLoading = false }
+    },
+    handleTabClick(tab) { if (tab.name === 'compare') this.$nextTick(this.renderCompareChart) },
     async loadCompare() {
       const supplierNames = this.selectedSuppliers.slice(0, 5).map(item => item.supplierName).join(',')
       this.compareLoading = true
       try {
-        const res = await getSupplyMonitorCompare({
-          ...this.buildParams(),
-          supplierNames
-        })
+        const res = await getSupplyMonitorCompare({ ...this.buildParams(), supplierNames })
         this.compareList = Array.isArray(res.data) ? res.data : []
-        this.pagination.compare.page = 1
         this.activeTab = 'compare'
         this.$nextTick(this.renderCompareChart)
-      } finally {
-        this.compareLoading = false
-      }
+      } finally { this.compareLoading = false }
+    },
+    handleSelectionChange(selection) { this.selectedSuppliers = selection.slice(0, 5) },
+    matchLocalFilters(item) {
+      if (this.localFilters.supplierLevel && item.supplierLevel !== this.localFilters.supplierLevel) return false
+      if (this.localFilters.warningTag && item.warningTag !== this.localFilters.warningTag) return false
+      return this.inRange(item.totalScore, this.localFilters.minTotalScore, this.localFilters.maxTotalScore, 1) &&
+        this.inRange(item.termDays, this.localFilters.minTermDays, this.localFilters.maxTermDays, 1) &&
+        this.inRange(item.receiptAmount, this.localFilters.minReceiptAmount, this.localFilters.maxReceiptAmount, 1) &&
+        this.inRange(item.deliveryRate, this.localFilters.minDeliveryRate, this.localFilters.maxDeliveryRate, 100) &&
+        this.inRange(item.completionRate, this.localFilters.minCompletionRate, this.localFilters.maxCompletionRate, 100)
     },
-    handleSelectionChange(selection) {
-      this.selectedSuppliers = selection.slice(0, 5)
+    inRange(value, min, max, multiplier = 1) {
+      const number = Number(value || 0) * multiplier
+      if (min !== undefined && min !== null && min !== '' && number < Number(min)) return false
+      if (max !== undefined && max !== null && max !== '' && number > Number(max)) return false
+      return true
     },
     renderCompareChart() {
       const chartEl = this.$refs.compareChart
       if (!chartEl) return
       this.compareChart = echarts.getInstanceByDom(chartEl) || echarts.init(chartEl)
       const names = this.compareList.map(item => item.supplierName)
+      const labelOrderAmount = '\u8ba2\u5355\u91d1\u989d'
+      const labelReceiptAmount = '\u5165\u5e93\u91d1\u989d'
+      const labelDeliveryRate = '\u4ea4\u4ed8\u53ca\u65f6\u7387'
+      const labelCompletionRate = '\u5b8c\u6210\u7387'
+      const roundedBar = { borderRadius: [10, 10, 0, 0] }
+      const barBase = { type: 'bar', barMaxWidth: 46, barGap: '10%', barCategoryGap: '18%', emphasis: { focus: 'series' } }
+      const gradient = (top, bottom) => new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+        { offset: 0, color: top },
+        { offset: 1, color: bottom }
+      ])
       this.compareChart.setOption({
-        color: ['#409eff', '#67c23a', '#e6a23c', '#f56c6c'],
-        tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
-        legend: { top: 0, data: ['订单金额', '入库金额', '交付及时率', '完成率'] },
-        grid: { left: 56, right: 24, top: 48, bottom: 64 },
+        backgroundColor: 'transparent',
+        color: ['#3b82f6', '#14b8a6', '#f59e0b', '#ef4444'],
+        tooltip: {
+          trigger: 'axis',
+          axisPointer: { type: 'shadow', shadowStyle: { color: 'rgba(72, 92, 138, 0.08)' } },
+          backgroundColor: 'rgba(255,255,255,0.96)',
+          borderColor: '#dbe5f5',
+          borderWidth: 1,
+          textStyle: { color: '#25324d' },
+          extraCssText: 'box-shadow:0 12px 30px rgba(27,39,71,.12);border-radius:8px;'
+        },
+        legend: {
+          top: 4,
+          itemWidth: 18,
+          itemHeight: 10,
+          icon: 'roundRect',
+          textStyle: { color: '#59667f' },
+          data: [labelOrderAmount, labelReceiptAmount, labelDeliveryRate, labelCompletionRate]
+        },
+        grid: { left: 62, right: 48, top: 58, bottom: 58 },
         xAxis: {
           type: 'category',
           data: names,
-          axisLabel: { interval: 0, rotate: names.length > 3 ? 20 : 0 }
+          axisTick: { show: false },
+          axisLine: { lineStyle: { color: '#d7deea' } },
+          axisLabel: { color: '#6b7280', interval: 0, rotate: names.length > 3 ? 18 : 0 }
         },
         yAxis: [
-          { type: 'value', name: '金额', axisLabel: { formatter: value => this.compactMoney(value) } },
-          { type: 'value', name: '比例', min: 0, max: 100, axisLabel: { formatter: '{value}%' } }
+          {
+            type: 'value',
+            name: '\u91d1\u989d',
+            axisLine: { show: false },
+            axisTick: { show: false },
+            splitLine: { lineStyle: { color: '#eef2f8' } },
+            axisLabel: { color: '#6b7280', formatter: value => this.compactMoney(value) }
+          },
+          {
+            type: 'value',
+            name: '\u6bd4\u4f8b',
+            min: 0,
+            max: 100,
+            axisLine: { show: false },
+            axisTick: { show: false },
+            splitLine: { show: false },
+            axisLabel: { color: '#6b7280', formatter: '{value}%' }
+          }
         ],
         series: [
-          { name: '订单金额', type: 'bar', data: this.compareList.map(item => Number(item.orderAmount || 0)) },
-          { name: '入库金额', type: 'bar', data: this.compareList.map(item => Number(item.receiptAmount || 0)) },
-          { name: '交付及时率', type: 'bar', yAxisIndex: 1, data: this.compareList.map(item => Number(item.deliveryRate || 0) * 100) },
-          { name: '完成率', type: 'bar', yAxisIndex: 1, data: this.compareList.map(item => Number(item.completionRate || 0) * 100) }
+          { ...barBase, name: labelOrderAmount, data: this.compareList.map(item => Number(item.orderAmount || 0)), itemStyle: { ...roundedBar, color: gradient('#60a5fa', '#2563eb') } },
+          { ...barBase, name: labelReceiptAmount, data: this.compareList.map(item => Number(item.receiptAmount || 0)), itemStyle: { ...roundedBar, color: gradient('#2dd4bf', '#059669') } },
+          { ...barBase, name: labelDeliveryRate, yAxisIndex: 1, data: this.compareList.map(item => Number(item.deliveryRate || 0) * 100), itemStyle: { ...roundedBar, color: gradient('#fbbf24', '#f59e0b') } },
+          { ...barBase, name: labelCompletionRate, yAxisIndex: 1, data: this.compareList.map(item => Number(item.completionRate || 0) * 100), itemStyle: { ...roundedBar, color: gradient('#fb7185', '#ef4444') } }
         ]
       }, true)
       this.compareChart.resize()
     },
-    handleResize() {
-      if (this.compareChart) this.compareChart.resize()
-    },
-    paymentTag(status) {
-      if (status === '已到期') return 'danger'
-      if (status === '7天内到期') return 'warning'
-      return 'success'
+    handleResize() { if (this.compareChart) this.compareChart.resize() },
+    levelCount(level) { return this.supplierList.filter(item => item.supplierLevel === level).length },
+    levelTag(level) { return level === 'A' ? 'success' : level === 'B' ? '' : level === 'C' ? 'warning' : 'danger' },
+    warningTagType(tag) { return tag === '正常' ? 'success' : (tag === '综合分预警' || tag === '质量预警') ? 'danger' : 'warning' },
+    paymentTag(status) { return status === '已到期' ? 'danger' : status === '7天内到期' ? 'warning' : 'success' },
+    paySuggestion(status) { return status === '已到期' ? '建议付款' : status === '7天内到期' ? '本期安排' : '暂缓' },
+    paySuggestionType(status) { return status === '已到期' ? 'danger' : status === '7天内到期' ? 'warning' : 'info' },
+    warningSuggestion(row) {
+      if (row.warningTag === '综合分预警') return '复核成本、交付和账期,必要时降低合作优先级。'
+      if (row.warningTag === '交付预警') return '跟进延期原因,重点检查计划交货日和实际验收差异。'
+      if (row.warningTag === '完成率预警') return '核对未完成数量,必要时拆分补单或调整采购计划。'
+      return '保持观察。'
     },
-    compactMoney(value) {
-      const number = Number(value || 0)
-      if (Math.abs(number) >= 10000) {
-        return `${this.formatNumber(number / 10000)}万`
-      }
-      return this.formatNumber(number)
-    },
-    formatNumber(value) {
-      return new Intl.NumberFormat('zh-CN', { maximumFractionDigits: 2 }).format(Number(value || 0))
-    },
-    formatMoney(value) {
-      return new Intl.NumberFormat('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(Number(value || 0))
-    },
-    formatPercent(value) {
-      return `${this.formatNumber(Number(value || 0) * 100)}%`
-    }
+    compactMoney(value) { const number = Number(value || 0); return Math.abs(number) >= 10000 ? `${this.formatNumber(number / 10000)}万` : this.formatNumber(number) },
+    formatNumber(value) { return new Intl.NumberFormat('zh-CN', { maximumFractionDigits: 2 }).format(Number(value || 0)) },
+    formatMoney(value) { return new Intl.NumberFormat('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(Number(value || 0)) },
+    formatPercent(value) { return `${this.formatNumber(Number(value || 0) * 100)}%` }
   }
 }
 </script>
 
 <style lang="scss" scoped>
-.app-container {
-  padding: 20px;
-}
-
-.page-header {
-  margin-bottom: 20px;
-
-  h2 {
-    font-size: 24px;
-    font-weight: 600;
-    color: #303133;
-    margin-bottom: 8px;
-
-    i {
-      margin-right: 8px;
-      color: #409eff;
-    }
-  }
-
-  .page-desc {
-    color: #909399;
-    font-size: 14px;
-    margin: 0;
-  }
-}
-
-.box-card {
-  margin-bottom: 20px;
-}
-
-.form-inline {
-  display: flex;
-  align-items: center;
-  flex-wrap: wrap;
-}
-
-.toolbar {
-  display: flex;
-  align-items: center;
-  justify-content: space-between;
-  margin-bottom: 12px;
-  color: #606266;
-}
-
-.chart-card {
-  position: relative;
-  margin-bottom: 16px;
-  padding: 16px;
-  border: 1px solid #ebeef5;
-  border-radius: 4px;
-  background: #fff;
-}
-
-.chart-title {
-  margin-bottom: 8px;
-  color: #303133;
-  font-weight: 600;
-}
-
-.compare-chart {
-  width: 100%;
-  height: 320px;
-}
-
-.chart-empty {
-  position: absolute;
-  left: 0;
-  right: 0;
-  top: 154px;
-  z-index: 2;
-  text-align: center;
-  color: #909399;
-}
-
-.pagination-wrap {
-  display: flex;
-  justify-content: flex-end;
-  padding-top: 16px;
-}
-
-::v-deep .el-card__header {
-  font-weight: bold;
-}
+.app-container { padding: 20px; }
+.page-header { margin-bottom: 20px; h2 { font-size: 24px; font-weight: 600; color: #303133; margin-bottom: 8px; i { margin-right: 8px; color: #409eff; } } .page-desc { color: #909399; font-size: 14px; margin: 0; } }
+.metric-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 14px; margin-bottom: 18px; }
+.metric-card { padding: 18px 20px; border: 1px solid #e8edf7; border-radius: 8px; background: #fff; box-shadow: 0 8px 22px rgba(31, 45, 61, 0.05); span, em { display: block; color: #7b8498; font-size: 12px; font-style: normal; } strong { display: block; margin: 8px 0 6px; color: #101b33; font-size: 26px; line-height: 1; } &.primary { border-top: 3px solid #3b82f6; } &.success { border-top: 3px solid #10b981; } &.warning { border-top: 3px solid #f59e0b; } &.danger { border-top: 3px solid #ef4444; } }
+.box-card { margin-bottom: 20px; }
+.form-inline { display: flex; align-items: center; flex-wrap: wrap; }
+.filter-divider { height: 1px; margin: 6px 0 16px; background: #eef1f7; }
+.filter-title { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; color: #101b33; font-size: 14px; font-weight: 700; em { color: #8a93a6; font-size: 12px; font-style: normal; font-weight: 500; } }
+.range-input { display: inline-flex; align-items: center; gap: 8px; padding: 0 10px; border-radius: 8px; background: #f6f8ff; span { color: #8a93a6; font-size: 12px; white-space: nowrap; } ::v-deep .el-input-number { width: 74px; line-height: 36px; } ::v-deep .el-input__inner { padding: 0; text-align: center; box-shadow: none; } &.wide ::v-deep .el-input-number { width: 104px; } }
+.toolbar { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; color: #606266; }
+.chart-card { position: relative; margin-bottom: 16px; padding: 20px 22px 18px; border: 1px solid #e7edf7; border-radius: 12px; background: linear-gradient(180deg, #ffffff 0%, #fbfdff 100%); box-shadow: 0 12px 30px rgba(31, 45, 61, .06); }
+.chart-title { margin-bottom: 8px; color: #071a3d; font-size: 16px; font-weight: 700; }
+.compare-chart { width: 100%; height: 380px; }
+.chart-empty { position: absolute; left: 0; right: 0; top: 154px; z-index: 2; text-align: center; color: #909399; }
+.summary-strip { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 10px; margin-bottom: 14px; }
+.summary-item { padding: 12px 14px; border: 1px solid #e8edf7; border-radius: 8px; background: #fbfcff; span, em { display: block; color: #7b8498; font-size: 12px; font-style: normal; } strong { display: block; margin: 5px 0; color: #101b33; font-size: 18px; } }
+.pagination-wrap { display: flex; justify-content: flex-end; padding-top: 16px; }
+::v-deep .el-card__header { font-weight: bold; }
+@media (max-width: 1200px) { .metric-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
+@media (max-width: 768px) { .metric-grid { grid-template-columns: 1fr; } }
 </style>

+ 482 - 188
src/views/supply/overall/index.vue

@@ -1,23 +1,21 @@
-<template>
-  <div class="home">
+<template>
+  <div class="home supply-dashboard-page">
     <div class="page-header">
       <div class="page-title">供应商能力综合评分面板</div>
       <div class="page-subtitle">
-        实时监控供应商表现,智能分析综合能力,辅助决策优化供应链
+        实时监控供应商表现,按成本、交付、账期三个维度评估综合能力。
       </div>
     </div>
 
-    <!-- Stats Grid -->
     <div class="stats-grid">
       <div class="stat-card">
         <div class="stat-header">
           <div class="stat-label">供应商总数</div>
           <div class="stat-badge badge-time">实时</div>
         </div>
-        <div class="stat-value">156</div>
+        <div class="stat-value">{{ overviewStats.supplierTotal }}</div>
         <div class="stat-trend trend-up">
-          <span>↑ 6.2%</span>
-          <span style="color: #999">较上月</span>
+          <span>数据库统计</span>
         </div>
       </div>
 
@@ -26,10 +24,9 @@
           <div class="stat-label">平均综合得分</div>
           <div class="stat-badge badge-time">实时</div>
         </div>
-        <div class="stat-value">82.5</div>
+        <div class="stat-value">{{ formatNumber(overviewStats.averageScore, 1) }}</div>
         <div class="stat-trend trend-up">
-          <span>↑ 3.8%</span>
-          <span style="color: #999">较上月</span>
+          <span>成本40% / 交付40% / 账期20%</span>
         </div>
       </div>
 
@@ -38,10 +35,9 @@
           <div class="stat-label">预警供应商</div>
           <div class="stat-badge badge-warning">预警</div>
         </div>
-        <div class="stat-value">8</div>
+        <div class="stat-value">{{ overviewStats.warningSupplierCount }}</div>
         <div class="stat-trend trend-down">
-          <span>↓ 2</span>
-          <span style="color: #999">低于60分</span>
+          <span>低于60分</span>
         </div>
       </div>
 
@@ -50,66 +46,102 @@
           <div class="stat-label">优秀供应商</div>
           <div class="stat-badge badge-time">实时</div>
         </div>
-        <div class="stat-value">45</div>
+        <div class="stat-value">{{ overviewStats.excellentSupplierCount }}</div>
         <div class="stat-trend trend-up">
-          <span>↑ 5</span>
-          <span style="color: #999">≥85分</span>
+          <span>85分及以上</span>
         </div>
       </div>
     </div>
 
-    <!-- Search and Chart Section -->
     <div class="content-card">
-      <div class="card-title">🔍 单个产品查询供应商评分</div>
+      <div class="card-title">单个产品查询供应商评分</div>
       <div class="search-box">
         <div class="input-wrapper">
           <input
             v-model="inputProductCode"
             type="text"
             class="search-input"
-            placeholder="输入产品编码(如:20220606J0100MR4)"
+            placeholder="输入产品编码(如:J06A14BS1)"
+            @keyup.enter="triggerEvaluation"
           />
         </div>
         <button @click="triggerEvaluation" class="search-btn">预测分析</button>
       </div>
 
       <div class="chart-container">
-        <div style="font-size: 14px; color: #666; margin-bottom: 15px">
-          产品供应商三维度评分趋势对比
-        </div>
-        <div class="chart-placeholder">
-          <svg
-            class="chart-lines"
-            viewBox="0 0 800 300"
-            preserveAspectRatio="none"
-          >
-            <polyline
-              points="50,250 150,180 250,120 350,80 450,90 550,110 650,140 750,200"
-              fill="none"
-              stroke="rgba(255,255,255,0.6)"
-              stroke-width="3"
-            />
-            <polyline
-              points="50,270 150,220 250,160 350,100 450,95 550,120 650,160 750,220"
-              fill="none"
-              stroke="rgba(255,255,255,0.4)"
-              stroke-width="2"
-              stroke-dasharray="5,5"
-            />
-            <polyline
-              points="50,280 150,240 250,200 350,140 450,130 550,150 650,180 750,240"
-              fill="none"
-              stroke="rgba(255,255,255,0.4)"
-              stroke-width="2"
-              stroke-dasharray="5,5"
-            />
+        <div v-if="!evaluationData.length" class="chart-placeholder">
+          <svg class="chart-lines" viewBox="0 0 800 300" preserveAspectRatio="none">
+            <polyline points="50,250 150,180 250,120 350,80 450,90 550,110 650,140 750,200" fill="none" stroke="rgba(255,255,255,0.6)" stroke-width="3" />
+            <polyline points="50,270 150,220 250,160 350,100 450,95 550,120 650,160 750,220" fill="none" stroke="rgba(255,255,255,0.4)" stroke-width="2" stroke-dasharray="5,5" />
+            <polyline points="50,280 150,240 250,200 350,140 450,130 550,150 650,180 750,240" fill="none" stroke="rgba(255,255,255,0.4)" stroke-width="2" stroke-dasharray="5,5" />
           </svg>
-          <div style="position: relative; z-index: 1">
-            <div style="font-size: 14px; opacity: 0.8; margin-bottom: 5px">
-              选择产品查看供应商评分趋势
+          <div class="chart-empty-state">
+            <div>选择产品查看供应商评分趋势</div>
+            <div>蓝线:成本得分 / 红线:交付得分 / 绿线:账期得分</div>
+          </div>
+        </div>
+
+        <div v-else class="score-panel">
+          <div class="score-panel-header">
+            <div>
+              <div class="chart-title-main">供应商三维度评分趋势</div>
+              <div class="chart-title-sub">
+                <span>产品编码: {{ displayedProductCode }}</span>
+                <span>最佳供应商: {{ bestSupplierName }}</span>
+              </div>
             </div>
-            <div style="font-size: 12px; opacity: 0.6">
-              蓝线:成本得分 | 红线:交付得分 | 绿线:账期得分
+            <div class="score-metrics">
+              <div class="score-metric">
+                <span>最高综合分</span>
+                <strong>{{ formatNumber(bestTotalScore, 2) }}</strong>
+              </div>
+              <div class="score-metric soft">
+                <span>供应商数</span>
+                <strong>{{ chartSuppliers.length }}</strong>
+              </div>
+            </div>
+          </div>
+
+          <div class="score-panel-body">
+            <div class="score-plot">
+              <div class="chart-legend">
+                <span><i class="legend-dot cost"></i>成本均分 {{ formatNumber(avgScore('costScore'), 1) }}</span>
+                <span><i class="legend-dot delivery"></i>交付均分 {{ formatNumber(avgScore('deliveryScore'), 1) }}</span>
+                <span><i class="legend-dot term"></i>账期均分 {{ formatNumber(avgScore('termScore'), 1) }}</span>
+              </div>
+              <svg class="score-chart" viewBox="0 0 920 320" preserveAspectRatio="none">
+                <defs>
+                  <linearGradient id="costStroke" x1="0" x2="1" y1="0" y2="0">
+                    <stop offset="0%" stop-color="#6366f1" />
+                    <stop offset="100%" stop-color="#8b5cf6" />
+                  </linearGradient>
+                  <linearGradient id="deliveryStroke" x1="0" x2="1" y1="0" y2="0">
+                    <stop offset="0%" stop-color="#14b8a6" />
+                    <stop offset="100%" stop-color="#22c55e" />
+                  </linearGradient>
+                  <linearGradient id="termStroke" x1="0" x2="1" y1="0" y2="0">
+                    <stop offset="0%" stop-color="#f59e0b" />
+                    <stop offset="100%" stop-color="#f97316" />
+                  </linearGradient>
+                  <linearGradient id="costArea" x1="0" x2="0" y1="0" y2="1">
+                    <stop offset="0%" stop-color="#6366f1" stop-opacity="0.14" />
+                    <stop offset="100%" stop-color="#6366f1" stop-opacity="0" />
+                  </linearGradient>
+                </defs>
+                <line v-for="tick in chartTicks" :key="tick" x1="56" x2="884" :y1="chartY(tick)" :y2="chartY(tick)" class="grid-line" />
+                <text v-for="tick in chartTicks" :key="`label-${tick}`" x="20" :y="chartY(tick) + 4" class="axis-label">{{ tick }}</text>
+                <polygon :points="areaPoints('costScore')" class="score-area" />
+                <polyline :points="polylinePoints('costScore')" class="score-line cost-line" />
+                <polyline :points="polylinePoints('deliveryScore')" class="score-line delivery-line" />
+                <polyline :points="polylinePoints('termScore')" class="score-line term-line" />
+              </svg>
+
+              <div class="chart-labels">
+                <span v-for="(supplier, index) in chartSuppliers" :key="supplier.supplierCode || index">
+                  <i>{{ index + 1 }}</i>
+                  {{ shortSupplierName(supplier.supplierName, index) }}
+                </span>
+              </div>
             </div>
           </div>
         </div>
@@ -118,90 +150,70 @@
       <div class="info-grid">
         <div class="info-box">
           <div class="info-label">评估结果</div>
-          <div class="info-value" style="color: #52c41a">已加载</div>
+          <div class="info-value" style="color: #52c41a">{{ evaluationData.length ? '已加载' : '待查询' }}</div>
           <div class="info-detail">
-            供应商数量: 5家<br />
-            评估完成时间: 2025-10-29 14:30<br />
-            <span style="color: #1890ff">最佳供应商: 华信电子</span>
+            供应商数量: {{ evaluationData.length }}家<br />
+            产品编码: {{ displayedProductCode || '-' }}<br />
+            <span style="color: #1890ff">最佳供应商: {{ bestSupplierName }}</span>
           </div>
         </div>
 
         <div class="info-box">
-          <div class="info-label">数据周期</div>
-          <div class="info-value">180天</div>
+          <div class="info-label">数据来源</div>
+          <div class="info-value">MySQL</div>
           <div class="info-detail">
-            评估订单总数: 238笔<br />
-            时间范围: 2025-05-01 至 2025-10-28
+            采购入库、到货匹配、供应商账期<br />
+            实时读取数据库统计
           </div>
         </div>
 
         <div class="info-box">
           <div class="info-label">维度评分范围</div>
-          <div class="info-value">80-95分</div>
+          <div class="info-value">0-100分</div>
           <div class="info-detail">
-            成本: 80-95分<br />
-            交付: 85-94分<br />
-            账期: 80-90分
+            成本: 价格排名<br />
+            交付: 准时率与数量满足率<br />
+            账期: 结算期限天数
           </div>
         </div>
 
         <div class="info-box">
           <div class="info-label">综合评级分布</div>
-          <div class="info-value">优秀: 5家</div>
+          <div class="info-value">优秀: {{ overviewStats.excellentSupplierCount }}家</div>
           <div class="info-detail">
-            A级(&ge;85分): 3家<br />
-            B级(70-85分): 2家<br />
-            C级(&lt;70分): 0家
+            A级(≥85分): {{ overviewStats.excellentSupplierCount }}家<br />
+            预警(&lt;60分): {{ overviewStats.warningSupplierCount }}家
           </div>
         </div>
       </div>
     </div>
 
-    <!-- Weights Configuration Summary -->
     <div class="content-card">
-      <div class="card-title">📊 评估维度权重配置</div>
+      <div class="card-title">评估维度权重配置</div>
       <div style="margin-bottom: 20px; color: #666; font-size: 14px">
         当前配置:成本 40% | 交付 40% | 账期 20%
       </div>
-      <div
-        style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px"
-      >
+      <div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px">
         <div class="info-box">
-          <div class="info-label">💰 成本维度</div>
+          <div class="info-label">成本维度</div>
           <div class="info-value" style="color: #5b6cff">40%</div>
-          <div class="info-detail">
-            评分规则:价格排名<br />
-            第1名: 100分<br />
-            第2名: 90分<br />
-            第3名: 80分
-          </div>
+          <div class="info-detail">按同一产品下供应商参考价排名评分</div>
         </div>
         <div class="info-box">
-          <div class="info-label">🚚 交付维度</div>
+          <div class="info-label">交付维度</div>
           <div class="info-value" style="color: #52c41a">40%</div>
-          <div class="info-detail">
-            D1-准时率: 50%<br />
-            D2-平均偏差: 30%<br />
-            D3-最长延迟: 10%<br />
-            D4-数量满足率: 10%
-          </div>
+          <div class="info-detail">准时率与数量满足率综合评分</div>
         </div>
         <div class="info-box">
-          <div class="info-label">💳 账期维度</div>
+          <div class="info-label">账期维度</div>
           <div class="info-value" style="color: #ff9800">20%</div>
-          <div class="info-detail">
-            ≥90天: 100分<br />
-            ≥60天: 90分<br />
-            ≥45天: 80分<br />
-            ≥30天: 60分
-          </div>
+          <div class="info-detail">账期越长得分越高</div>
         </div>
       </div>
     </div>
 
-    <!-- Data Statistics -->
     <div class="content-card">
-      <div class="card-title">📋 评估数据统计</div>
+      <div class="card-title">评估数据统计</div>
       <table class="supplier-table">
         <thead>
           <tr>
@@ -215,61 +227,38 @@
         </thead>
         <tbody>
           <tr>
-            <td><strong>💰 成本维度</strong></td>
-            <td>采购订单统计.xlsx</td>
-            <td>供应商报价排名</td>
-            <td>2,845条</td>
-            <td>
-              <div class="progress-bar">
-                <div class="progress-fill" style="width: 98.5%"></div>
-              </div>
-              <span style="font-size: 12px; color: #666">98.5%</span>
-            </td>
-            <td><span style="color: #52c41a">● 正常</span></td>
+            <td><strong>成本维度</strong></td>
+            <td>dtm_supply_purchase_receipt</td>
+            <td>供应商参考价排名</td>
+            <td>数据库实时统计</td>
+            <td><div class="progress-bar"><div class="progress-fill" style="width: 98.5%"></div></div><span style="font-size: 12px; color: #666">98.5%</span></td>
+            <td><span style="color: #52c41a">正常</span></td>
           </tr>
           <tr>
-            <td><strong>🚚 交付维度</strong></td>
-            <td>采购数据_双键合并结果.xlsx</td>
-            <td>准时率、偏差、延迟、满足率</td>
-            <td>5,126条</td>
-            <td>
-              <div class="progress-bar">
-                <div class="progress-fill" style="width: 99.2%"></div>
-              </div>
-              <span style="font-size: 12px; color: #666">99.2%</span>
-            </td>
-            <td><span style="color: #52c41a">● 正常</span></td>
+            <td><strong>交付维度</strong></td>
+            <td>dtm_supply_order_receipt_match</td>
+            <td>准时率、数量满足率</td>
+            <td>数据库实时统计</td>
+            <td><div class="progress-bar"><div class="progress-fill" style="width: 99.2%"></div></div><span style="font-size: 12px; color: #666">99.2%</span></td>
+            <td><span style="color: #52c41a">正常</span></td>
           </tr>
           <tr>
-            <td><strong>💳 账期维度</strong></td>
-            <td>供应商账期.xlsx</td>
+            <td><strong>账期维度</strong></td>
+            <td>dtm_supply_supplier_term</td>
             <td>结算期限天数</td>
-            <td>156条</td>
-            <td>
-              <div class="progress-bar">
-                <div class="progress-fill" style="width: 100%"></div>
-              </div>
-              <span style="font-size: 12px; color: #666">100%</span>
-            </td>
-            <td><span style="color: #52c41a">● 正常</span></td>
+            <td>数据库实时统计</td>
+            <td><div class="progress-bar"><div class="progress-fill" style="width: 100%"></div></div><span style="font-size: 12px; color: #666">100%</span></td>
+            <td><span style="color: #52c41a">正常</span></td>
           </tr>
         </tbody>
       </table>
     </div>
 
-    <!-- Top 5 Suppliers -->
     <div class="content-card">
       <div class="card-title">
-        🏆 Top 5 供应商综合排名
-        <span
-          style="
-            font-size: 12px;
-            color: #999;
-            font-weight: normal;
-            margin-left: 10px;
-          "
-        >
-          基于产品代码: {{ displayedProductCode || "20220606J0100MR4" }}
+        Top 5 供应商综合排名
+        <span style="font-size: 12px; color: #999; font-weight: normal; margin-left: 10px">
+          基于产品代码: {{ displayedProductCode || 'J06A14BS1' }}
         </span>
       </div>
 
@@ -289,45 +278,23 @@
             </tr>
           </thead>
           <tbody>
-            <tr
-              v-for="(supplier, index) in evaluationData"
-              :key="supplier.供应商代码"
-            >
-              <td>
-                <span :class="['rank-badge', getRankClass(index + 1)]">{{
-                  index + 1
-                }}</span>
-              </td>
-              <td>
-                <strong>{{ supplier.供应商名称 }}</strong>
-              </td>
-              <td>{{ supplier.供应商代码 || "N/A" }}</td>
+            <tr v-for="(supplier, index) in evaluationData" :key="supplier.supplierCode || index">
+              <td><span :class="['rank-badge', getRankClass(index + 1)]">{{ index + 1 }}</span></td>
+              <td><strong>{{ supplier.supplierName || '-' }}</strong></td>
+              <td>{{ supplier.supplierCode || 'N/A' }}</td>
               <td>
-                <strong style="font-size: 18px; color: #52c41a">{{
-                  parseFloat(supplier.综合得分).toFixed(2)
-                }}</strong>
-                <div class="progress-bar">
-                  <div
-                    class="progress-fill"
-                    :style="{ width: `${supplier.综合得分}%` }"
-                  ></div>
-                </div>
+                <strong style="font-size: 18px; color: #52c41a">{{ formatNumber(supplier.totalScore, 2) }}</strong>
+                <div class="progress-bar"><div class="progress-fill" :style="{ width: `${supplier.totalScore || 0}%` }"></div></div>
               </td>
               <td>
                 <div class="dimension-scores">
-                  <span class="dim-score"
-                    >成本:{{ parseFloat(supplier.成本分数).toFixed(0) }}</span
-                  >
-                  <span class="dim-score"
-                    >交付:{{ parseFloat(supplier.交付分数).toFixed(0) }}</span
-                  >
-                  <span class="dim-score"
-                    >账期:{{ parseFloat(supplier.账期分数).toFixed(0) }}</span
-                  >
+                  <span class="dim-score">成本:{{ formatNumber(supplier.costScore, 0) }}</span>
+                  <span class="dim-score">交付:{{ formatNumber(supplier.deliveryScore, 0) }}</span>
+                  <span class="dim-score">账期:{{ formatNumber(supplier.termScore, 0) }}</span>
                 </div>
               </td>
-              <td>¥{{ parseFloat(supplier.参考价).toFixed(2) }}</td>
-              <td>第{{ supplier.成本排名 }}名</td>
+              <td>{{ formatNumber(supplier.referencePrice, 2) }}</td>
+              <td>第{{ supplier.costRank || '-' }}名</td>
             </tr>
           </tbody>
         </table>
@@ -337,7 +304,7 @@
 </template>
 
 <script>
-import { evaluateSupplier } from "@/api/supply";
+import { evaluateSupplier, getSupplyOverviewStats } from "@/api/supply";
 
 export default {
   name: "SupplyOverallEvaluation",
@@ -348,37 +315,56 @@ export default {
       evaluationData: [],
       loading: false,
       error: "",
+      overviewStats: {
+        supplierTotal: 0,
+        averageScore: 0,
+        warningSupplierCount: 0,
+        excellentSupplierCount: 0,
+      },
     };
   },
+  computed: {
+    bestSupplierName() {
+      return this.evaluationData.length ? this.evaluationData[0].supplierName : "-";
+    },
+    bestTotalScore() {
+      return this.evaluationData.reduce((best, item) => Math.max(best, Number(item.totalScore || 0)), 0);
+    },
+    chartSuppliers() {
+      return this.evaluationData.slice(0, 5);
+    },
+    chartTicks() {
+      return [0, 25, 50, 75, 100];
+    },
+  },
   created() {
-    // 如果有传入的 productCode,则自动触发评估
+    this.fetchOverviewStats();
     if (this.$route.query.productCode) {
       this.inputProductCode = this.$route.query.productCode;
       this.fetchEvaluationData(this.inputProductCode);
     }
   },
   methods: {
-    // 监听路由传入的 productCode
+    async fetchOverviewStats() {
+      try {
+        const response = await getSupplyOverviewStats();
+        this.overviewStats = response && response.data ? response.data : response;
+      } catch (err) {
+        console.error("获取供应概览统计失败:", err);
+      }
+    },
     triggerEvaluation() {
       if (this.inputProductCode) {
         this.fetchEvaluationData(this.inputProductCode);
       }
     },
-
     async fetchEvaluationData(code) {
       this.loading = true;
       this.error = "";
       this.displayedProductCode = code;
       try {
         const response = await evaluateSupplier(code);
-        // Flask API可能直接返回数据,而不是包装在特定字段中
-        if (Array.isArray(response)) {
-          this.evaluationData = response;
-        } else if (response && response.data) {
-          this.evaluationData = response.data;
-        } else {
-          this.evaluationData = response || [];
-        }
+        this.evaluationData = Array.isArray(response) ? response : (response && response.data ? response.data : []);
         this.$modal.msgSuccess("评估完成");
       } catch (err) {
         console.error("获取评估数据失败:", err);
@@ -388,18 +374,48 @@ export default {
         this.loading = false;
       }
     },
-
-    // 获取排名样式类
+    formatNumber(value, digits) {
+      const number = parseFloat(value);
+      return Number.isFinite(number) ? number.toFixed(digits) : "0";
+    },
     getRankClass(rank) {
       if (rank === 1) return "rank-1";
       if (rank === 2) return "rank-2";
       if (rank === 3) return "rank-3";
       return "rank-other";
     },
+    chartX(index) {
+      const count = Math.max(this.chartSuppliers.length - 1, 1);
+      return 56 + (828 / count) * index;
+    },
+    chartY(value) {
+      const score = Math.max(0, Math.min(100, Number(value || 0)));
+      return 250 - score * 2;
+    },
+    polylinePoints(field) {
+      return this.chartSuppliers
+        .map((supplier, index) => `${this.chartX(index)},${this.chartY(supplier[field])}`)
+        .join(" ");
+    },
+    areaPoints(field) {
+      if (!this.chartSuppliers.length) return "";
+      const line = this.polylinePoints(field);
+      const firstX = this.chartX(0);
+      const lastX = this.chartX(this.chartSuppliers.length - 1);
+      return `${firstX},250 ${line} ${lastX},250`;
+    },
+    avgScore(field) {
+      if (!this.chartSuppliers.length) return 0;
+      const total = this.chartSuppliers.reduce((sum, item) => sum + Number(item[field] || 0), 0);
+      return total / this.chartSuppliers.length;
+    },
+    shortSupplierName(name, index) {
+      if (!name) return `供应商${index + 1}`;
+      return name.length > 8 ? `${name.slice(0, 8)}...` : name;
+    },
   },
 };
 </script>
-
 <style scoped>
 .page-header {
   margin-bottom: 40px;
@@ -614,6 +630,278 @@ export default {
   opacity: 0.2;
 }
 
+.score-panel {
+  min-height: 372px;
+  padding: 20px;
+  border: 1px solid rgba(98, 103, 242, 0.1);
+  border-radius: 20px;
+  background:
+    radial-gradient(circle at 92% 4%, rgba(99, 102, 241, 0.12), transparent 25%),
+    linear-gradient(180deg, #ffffff 0%, #fbfcff 100%);
+  box-shadow: 0 18px 42px rgba(42, 61, 115, 0.08);
+  animation: panelRiseIn 0.36s ease both;
+}
+
+.score-panel-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 20px;
+  margin-bottom: 14px;
+  padding: 2px 4px 0;
+}
+
+.chart-title-main {
+  color: #101b33;
+  font-size: 20px;
+  font-weight: 800;
+  line-height: 1.2;
+}
+
+.chart-title-sub {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 10px;
+  margin-top: 6px;
+  color: #8a93a6;
+  font-size: 12px;
+}
+
+.chart-title-sub span {
+  display: inline-flex;
+  align-items: center;
+  min-height: 22px;
+  padding: 0 9px;
+  border-radius: 999px;
+  background: #f6f8ff;
+  box-shadow: inset 0 0 0 1px rgba(98, 103, 242, 0.08);
+}
+
+.score-metrics {
+  display: flex;
+  gap: 10px;
+}
+
+.score-metric {
+  min-width: 108px;
+  padding: 10px 14px;
+  border-radius: 16px;
+  background: linear-gradient(180deg, #f8f7ff 0%, #f0f2ff 100%);
+  color: #7b8498;
+  text-align: right;
+  box-shadow: inset 0 0 0 1px rgba(98, 103, 242, 0.1);
+}
+
+.score-metric.soft {
+  background: linear-gradient(180deg, #f8fcff 0%, #eff8ff 100%);
+}
+
+.score-metric span {
+  display: block;
+  margin-bottom: 4px;
+  font-size: 12px;
+}
+
+.score-metric strong {
+  color: #101b33;
+  font-size: 22px;
+  line-height: 1;
+}
+
+.score-panel-body {
+  position: relative;
+  padding: 0;
+  border-radius: 18px;
+  background: linear-gradient(180deg, #ffffff 0%, #f9fbff 100%);
+  box-shadow:
+    inset 0 0 0 1px #edf0fb,
+    0 12px 26px rgba(42, 61, 115, 0.05);
+  overflow: hidden;
+}
+
+.chart-legend {
+  position: absolute;
+  z-index: 3;
+  top: 14px;
+  left: 18px;
+  display: flex;
+  flex-wrap: wrap;
+  gap: 8px;
+  margin: 0;
+  color: #5d667a;
+  font-size: 12px;
+  font-weight: 700;
+}
+
+.chart-legend span {
+  display: inline-flex;
+  align-items: center;
+  gap: 7px;
+  padding: 7px 10px;
+  border-radius: 999px;
+  background: rgba(255, 255, 255, 0.86);
+  backdrop-filter: blur(10px);
+  box-shadow:
+    inset 0 0 0 1px rgba(98, 103, 242, 0.08),
+    0 8px 18px rgba(42, 61, 115, 0.06);
+}
+
+.legend-dot {
+  width: 9px;
+  height: 9px;
+  border-radius: 50%;
+  background: #6366f1;
+}
+
+.legend-dot.delivery {
+  background: #14b8a6;
+}
+
+.legend-dot.term {
+  background: #f59e0b;
+}
+
+.score-plot {
+  position: relative;
+  height: 284px;
+  padding: 42px 18px 34px;
+}
+
+.score-chart {
+  width: 100%;
+  height: 226px;
+  display: block;
+}
+
+.grid-line {
+  stroke: rgba(105, 114, 135, 0.12);
+  stroke-width: 1;
+}
+
+.axis-label {
+  fill: #98a1b4;
+  font-size: 12px;
+  font-weight: 600;
+}
+
+.score-area {
+  fill: url(#costArea);
+  opacity: 0;
+  animation: chartFadeIn 0.7s ease 0.2s forwards;
+}
+
+.score-line {
+  fill: none;
+  stroke-width: 2.2;
+  stroke-linecap: round;
+  stroke-linejoin: round;
+  stroke-dasharray: 1200;
+  stroke-dashoffset: 1200;
+  filter: none;
+  animation: drawScoreLine 1.15s cubic-bezier(0.33, 1, 0.68, 1) forwards;
+}
+
+.cost-line {
+  stroke: url(#costStroke);
+}
+
+.delivery-line {
+  stroke: url(#deliveryStroke);
+  animation-delay: 0.12s;
+}
+
+.term-line {
+  stroke: url(#termStroke);
+  animation-delay: 0.24s;
+}
+
+.chart-labels {
+  position: absolute;
+  left: 74px;
+  right: 42px;
+  bottom: 14px;
+  display: flex;
+  justify-content: space-between;
+  gap: 12px;
+  color: #7b8498;
+  font-size: 12px;
+  font-weight: 600;
+}
+
+.chart-labels span {
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  gap: 5px;
+  max-width: 150px;
+  min-width: 0;
+  padding: 5px 8px;
+  border-radius: 999px;
+  background: #fff;
+  box-shadow:
+    inset 0 0 0 1px #edf0fb,
+    0 6px 14px rgba(42, 61, 115, 0.05);
+  overflow: hidden;
+  text-align: center;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.chart-labels i {
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  flex: 0 0 auto;
+  width: 17px;
+  height: 17px;
+  border-radius: 50%;
+  background: #f1efff;
+  color: #6267f2;
+  font-size: 11px;
+  font-style: normal;
+  font-weight: 800;
+}
+
+.chart-empty-state {
+  position: relative;
+  z-index: 1;
+  text-align: center;
+}
+
+.chart-empty-state div:first-child {
+  margin-bottom: 5px;
+  font-size: 14px;
+  opacity: 0.85;
+}
+
+.chart-empty-state div:last-child {
+  font-size: 12px;
+  opacity: 0.65;
+}
+
+@keyframes drawScoreLine {
+  to {
+    stroke-dashoffset: 0;
+  }
+}
+
+@keyframes chartFadeIn {
+  to {
+    opacity: 1;
+  }
+}
+
+@keyframes panelRiseIn {
+  from {
+    opacity: 0;
+    transform: translateY(8px);
+  }
+  to {
+    opacity: 1;
+    transform: translateY(0);
+  }
+}
+
 .info-grid {
   display: grid;
   grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
@@ -795,3 +1083,9 @@ export default {
   }
 }
 </style>
+
+
+
+
+
+

+ 105 - 29
src/views/supply/payment/index.vue

@@ -5,7 +5,6 @@
       <p class="page-desc">分析各供应商的结算账期条件</p>
     </div>
 
-    <!-- 搜索框 -->
     <el-card class="box-card">
       <div slot="header" class="clearfix">
         <span><i class="el-icon-search"></i> 查询产品账期分析</span>
@@ -20,14 +19,11 @@
           />
         </el-form-item>
         <el-form-item>
-          <el-button type="primary" @click="fetchPaymentData(inputProductCode)"
-            >分析账期</el-button
-          >
+          <el-button type="primary" @click="fetchPaymentData(inputProductCode)">分析账期</el-button>
         </el-form-item>
       </el-form>
     </el-card>
 
-    <!-- 表格数据 -->
     <el-card v-loading="loading" class="box-card">
       <div slot="header" class="clearfix">
         <span><i class="el-icon-document"></i> 账期详情表格</span>
@@ -47,24 +43,49 @@
         highlight-current-row
         style="width: 100%"
       >
-        <el-table-column prop="供应商名称" label="供应商名称" width="200" />
-        <el-table-column prop="账期分数" label="账期分数" width="120">
+        <el-table-column prop="供应商名称" label="供应商名称" min-width="220" show-overflow-tooltip />
+        <el-table-column prop="供应商代码" label="供应商代码" width="120" />
+        <el-table-column prop="维护状态" label="账期状态" width="100" align="center">
           <template slot-scope="scope">
-            {{ parseFloat(scope.row.账期分数).toFixed(2) }}
+            <el-tag :type="scope.row.维护状态 === '已维护' ? 'success' : 'danger'" size="mini">
+              {{ scope.row.维护状态 }}
+            </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column prop="结算期限" label="结算期限" min-width="160" show-overflow-tooltip>
+          <template slot-scope="scope">
+            {{ scope.row.结算期限 || "-" }}
+          </template>
+        </el-table-column>
+        <el-table-column prop="账期天数" label="账期天数" width="110" align="right">
+          <template slot-scope="scope">
+            {{ Number(scope.row.账期天数 || 0).toLocaleString("zh-CN") }}天
+          </template>
+        </el-table-column>
+        <el-table-column prop="账期等级" label="账期等级" width="100" align="center">
+          <template slot-scope="scope">
+            <el-tag :type="termLevelType(scope.row.账期等级)" size="mini">
+              {{ scope.row.账期等级 }}
+            </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column prop="账期分数" label="账期分数" width="120" align="right">
+          <template slot-scope="scope">
+            {{ formatNumber(scope.row.账期分数, 2) }}
           </template>
         </el-table-column>
       </el-table>
 
       <div v-else class="no-data">
         <p v-if="error" class="error">{{ error }}</p>
-        <p v-else>暂无账期数据,请输入产品编码进行分析</p>
+        <p v-else>{{ emptyText }}</p>
       </div>
     </el-card>
   </div>
 </template>
 
 <script>
-import { download } from "@/utils";
+import { saveAs } from "file-saver";
 import { getProductDetails } from "@/api/supply";
 
 export default {
@@ -75,17 +96,16 @@ export default {
       paymentDetails: [],
       loading: false,
       error: "",
+      emptyText: "暂无账期数据,请输入产品编码进行分析",
     };
   },
   created() {
-    // 如果有传入的 productCode,则自动触发分析
     if (this.$route.query.productCode) {
       this.inputProductCode = this.$route.query.productCode;
       this.fetchPaymentData(this.inputProductCode);
     }
   },
   methods: {
-    // 获取账期数据
     async fetchPaymentData(code) {
       if (!code) {
         this.$modal.msgError("请输入产品编码");
@@ -94,27 +114,36 @@ export default {
 
       this.loading = true;
       this.error = "";
+      this.emptyText = "正在查询账期数据...";
 
       try {
-        // 使用项目标准的request方式调用API
         const response = await getProductDetails(code);
+        const details =
+          response && response.payment_details !== undefined
+            ? response.payment_details
+            : response && response.data && response.data.payment_details !== undefined
+              ? response.data.payment_details
+              : Array.isArray(response)
+                ? response
+                : [];
 
-        // Flask API通常直接返回数据,没有code字段
-        if (response && response.payment_details !== undefined) {
-          this.paymentDetails = response.payment_details || [];
-          this.$modal.msgSuccess("账期分析完成");
-        } else {
-          // 尝试其他可能的数据结构
-          if (response && Array.isArray(response)) {
-            this.paymentDetails = response || [];
+        if (Array.isArray(details)) {
+          this.paymentDetails = this.normalizePaymentDetails(details);
+          if (this.paymentDetails.length > 0) {
+            this.emptyText = "";
             this.$modal.msgSuccess("账期分析完成");
           } else {
-            this.error = "获取账期数据失败,API响应格式不匹配";
-            this.$modal.msgError(this.error);
+            this.emptyText = `产品编码 ${code} 未匹配到账期数据,请确认该产品是否有关联供应商账期记录`;
+            this.$modal.msgWarning(this.emptyText);
           }
+        } else {
+          this.paymentDetails = [];
+          this.error = "获取账期数据失败,API响应格式不匹配";
+          this.$modal.msgError(this.error);
         }
       } catch (err) {
         console.error("获取账期数据失败:", err);
+        this.paymentDetails = [];
         this.error = err.message || "获取账期数据失败";
         this.$modal.msgError(this.error);
       } finally {
@@ -122,27 +151,74 @@ export default {
       }
     },
 
-    // 导出数据
+    normalizePaymentDetails(rows) {
+      return (Array.isArray(rows) ? rows : []).map((row) => {
+        const termDays = Number(row.账期天数 || row.termDays || 0);
+        const status = row.维护状态 || row.termStatus || (termDays > 0 ? "已维护" : "缺少账期");
+        const level = row.账期等级 || row.termLevel || this.termLevel(termDays);
+
+        return {
+          ...row,
+          供应商代码: row.供应商代码 || row.supplierCode || "-",
+          供应商名称: row.供应商名称 || row.supplierName || "-",
+          结算期限: row.结算期限 || row.paymentTerms || "",
+          账期天数: termDays,
+          账期分数: Number(row.账期分数 || row.termScore || 0),
+          账期等级: this.normalizeMissingText(level),
+          维护状态: this.normalizeMissingText(status),
+        };
+      });
+    },
+
+    normalizeMissingText(value) {
+      return value === "未维护" ? "缺少账期" : value;
+    },
+
+    formatNumber(value, digits = 2) {
+      const number = Number(value);
+      return Number.isFinite(number) ? number.toFixed(digits) : "0.00";
+    },
+
+    termLevel(days) {
+      if (days >= 90) return "优秀";
+      if (days >= 60) return "良好";
+      if (days >= 30) return "一般";
+      if (days > 0) return "偏短";
+      return "缺少账期";
+    },
+
+    termLevelType(level) {
+      if (level === "优秀" || level === "良好") return "success";
+      if (level === "一般") return "warning";
+      return "danger";
+    },
+
     exportData() {
       if (!this.paymentDetails || this.paymentDetails.length === 0) {
         this.$modal.msgError("暂无数据可导出");
         return;
       }
 
-      // 创建CSV内容
-      const header = ["供应商名称", "账期分数"].join(",");
+      const header = ["供应商名称", "供应商代码", "账期状态", "结算期限", "账期天数", "账期等级", "账期分数"].join(",");
       const csvContent = [
         header,
         ...this.paymentDetails.map((row) =>
-          [`"${row.供应商名称}"`, parseFloat(row.账期分数).toFixed(2)].join(",")
+          [
+            `"${row.供应商名称}"`,
+            row.供应商代码,
+            row.维护状态,
+            `"${row.结算期限 || ""}"`,
+            row.账期天数,
+            row.账期等级,
+            this.formatNumber(row.账期分数, 2),
+          ].join(",")
         ),
       ].join("\n");
 
-      // 下载文件
       const blob = new Blob(["\ufeff" + csvContent], {
         type: "text/csv;charset=utf-8;",
       });
-      download(blob, `供应商账期分析_${new Date().getTime()}.csv`);
+      saveAs(blob, `供应商账期分析_${new Date().getTime()}.csv`);
     },
   },
 };

+ 362 - 0
src/views/supply/qa-demo/index.vue

@@ -0,0 +1,362 @@
+<template>
+  <div class="app-container qa-demo-page">
+    <section class="hero-panel">
+      <div>
+        <p class="eyebrow">供应监测评分卡扩展方案</p>
+        <h1>QA 合格率质量评分演示</h1>
+        <p class="summary">
+          基于《2026年每日QA登记表》的抽检、不合格和大批次品数据,为供应商评分卡增加质量维度。
+          当前页面为静态演示,不依赖后端和数据库。
+        </p>
+      </div>
+      <div class="hero-score">
+        <span>新综合分</span>
+        <strong>成本30% + 交付30% + 账期15% + 质量25%</strong>
+      </div>
+    </section>
+
+    <section class="metric-grid">
+      <div class="metric-card blue">
+        <span>QA覆盖月份</span>
+        <strong>1-5月</strong>
+        <em>按全部 Sheet 汇总</em>
+      </div>
+      <div class="metric-card green">
+        <span>质量指标</span>
+        <strong>QA合格率</strong>
+        <em>由抽检数和不合格数推算</em>
+      </div>
+      <div class="metric-card violet">
+        <span>预警阈值</span>
+        <strong>&lt; 95%</strong>
+        <em>触发质量预警</em>
+      </div>
+      <div class="metric-card orange">
+        <span>无QA数据</span>
+        <strong>80分</strong>
+        <em>中性分,不直接扣为0</em>
+      </div>
+    </section>
+
+    <section class="content-grid">
+      <el-card class="box-card logic-card">
+        <div slot="header" class="section-title">
+          <i class="el-icon-s-operation"></i>
+          <span>计算逻辑</span>
+        </div>
+        <div class="formula-stack">
+          <div>抽检总数 = SUM(抽检/个数)</div>
+          <div>不合格总数 = SUM(不合格/个 + 大批次品)</div>
+          <div>QA合格率 = (抽检总数 - 不合格总数) / 抽检总数</div>
+          <div>质量分 = QA合格率 × 100</div>
+        </div>
+      </el-card>
+
+      <el-card class="box-card flow-card">
+        <div slot="header" class="section-title">
+          <i class="el-icon-connection"></i>
+          <span>数据流</span>
+        </div>
+        <div class="flow-line">
+          <span>QA Excel</span>
+          <i class="el-icon-right"></i>
+          <span>按月读取</span>
+          <i class="el-icon-right"></i>
+          <span>供应商汇总</span>
+          <i class="el-icon-right"></i>
+          <span>评分卡</span>
+        </div>
+        <p class="muted">
+          供应商名称采用宽松匹配,例如系统供应商全称包含“顾舒家华”,即可关联 QA 表中的简称。
+        </p>
+      </el-card>
+    </section>
+
+    <el-card class="box-card">
+      <div slot="header" class="section-title">
+        <i class="el-icon-s-data"></i>
+        <span>评分卡演示数据</span>
+      </div>
+      <el-table :data="demoRows" border highlight-current-row style="width: 100%">
+        <el-table-column prop="supplierName" label="供应商名称" min-width="220" fixed="left" show-overflow-tooltip />
+        <el-table-column label="综合分" width="100" align="right">
+          <template slot-scope="scope">
+            <strong>{{ formatNumber(scope.row.totalScore) }}</strong>
+          </template>
+        </el-table-column>
+        <el-table-column label="质量分" width="100" align="right">
+          <template slot-scope="scope">{{ formatNumber(scope.row.qualityScore) }}</template>
+        </el-table-column>
+        <el-table-column label="QA合格率" width="110" align="right">
+          <template slot-scope="scope">{{ scope.row.qaPassRate === null ? '暂无' : formatPercent(scope.row.qaPassRate) }}</template>
+        </el-table-column>
+        <el-table-column prop="qaSampleQty" label="抽检数" width="100" align="right" />
+        <el-table-column prop="qaDefectQty" label="不合格数" width="100" align="right" />
+        <el-table-column label="成本分" width="90" align="right">
+          <template slot-scope="scope">{{ formatNumber(scope.row.costScore) }}</template>
+        </el-table-column>
+        <el-table-column label="交付分" width="90" align="right">
+          <template slot-scope="scope">{{ formatNumber(scope.row.deliveryScore) }}</template>
+        </el-table-column>
+        <el-table-column label="账期分" width="90" align="right">
+          <template slot-scope="scope">{{ formatNumber(scope.row.termScore) }}</template>
+        </el-table-column>
+        <el-table-column label="预警" width="120" align="center">
+          <template slot-scope="scope">
+            <el-tag :type="warningTagType(scope.row.warningTag)" size="mini">{{ scope.row.warningTag }}</el-tag>
+          </template>
+        </el-table-column>
+      </el-table>
+    </el-card>
+
+    <section class="content-grid bottom-grid">
+      <el-card class="box-card">
+        <div slot="header" class="section-title">
+          <i class="el-icon-warning-outline"></i>
+          <span>预警规则</span>
+        </div>
+        <ul class="rule-list">
+          <li>QA抽检数大于 0,且 QA合格率低于 95%,标记为质量预警。</li>
+          <li>无 QA 数据时显示“暂无”,质量分按 80 分处理。</li>
+          <li>质量预警优先级高于交付预警和完成率预警。</li>
+        </ul>
+      </el-card>
+
+      <el-card class="box-card">
+        <div slot="header" class="section-title">
+          <i class="el-icon-document-checked"></i>
+          <span>业务说明</span>
+        </div>
+        <p class="muted large">
+          新增质量维度后,供应商不会只因为价格低或交付快而获得高分。
+          如果 QA 不合格率高,综合分会被拉低,并在评分卡中直接暴露质量风险。
+        </p>
+      </el-card>
+    </section>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'SupplyQaScoreDemo',
+  data() {
+    return {
+      demoRows: [
+        this.createRow('德清顾舒家华高分子材料有限公司', 82, 76, 100, 102, 98, 450, 9),
+        this.createRow('浙江华睿海绵制品有限公司', 88, 81, 90, 260, 93, 520, 36),
+        this.createRow('广州金凯迪家居用品有限公司', 100, 72, 60, null, 80, 0, 0),
+        this.createRow('湖州高裕家居科技有限公司', 90, 58, 90, 96, 94, 320, 19)
+      ]
+    }
+  },
+  methods: {
+    createRow(supplierName, costScore, deliveryScore, termScore, qaPassRate, qualityScore, qaSampleQty, qaDefectQty) {
+      const totalScore = costScore * 0.3 + deliveryScore * 0.3 + termScore * 0.15 + qualityScore * 0.25
+      return {
+        supplierName,
+        costScore,
+        deliveryScore,
+        termScore,
+        qaPassRate: qaPassRate === null ? null : qaPassRate / 100,
+        qualityScore,
+        qaSampleQty,
+        qaDefectQty,
+        totalScore,
+        warningTag: qaPassRate !== null && qaPassRate < 95 ? '质量预警' : totalScore < 60 ? '综合分预警' : '正常'
+      }
+    },
+    formatNumber(value, digits = 2) {
+      const number = Number(value)
+      return Number.isFinite(number) ? number.toFixed(digits) : '0.00'
+    },
+    formatPercent(value) {
+      return `${this.formatNumber(Number(value || 0) * 100)}%`
+    },
+    warningTagType(tag) {
+      if (tag === '正常') return 'success'
+      if (tag === '质量预警' || tag === '综合分预警') return 'danger'
+      return 'warning'
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.qa-demo-page {
+  padding: 20px;
+  background: #eef3f9;
+  min-height: calc(100vh - 84px);
+  color: #17233d;
+}
+
+.hero-panel {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  gap: 24px;
+  padding: 28px;
+  border-radius: 18px;
+  background: linear-gradient(135deg, #f8fbff 0%, #e8f0ff 100%);
+  margin-bottom: 18px;
+  box-shadow: 0 18px 40px rgba(30, 64, 175, 0.08);
+
+  h1 {
+    margin: 6px 0 10px;
+    font-size: 30px;
+    color: #071735;
+  }
+
+  .summary {
+    margin: 0;
+    color: #52627a;
+    max-width: 760px;
+    line-height: 1.8;
+  }
+}
+
+.eyebrow {
+  margin: 0;
+  font-size: 13px;
+  color: #4f63ff;
+  font-weight: 700;
+}
+
+.hero-score {
+  min-width: 300px;
+  padding: 18px 20px;
+  border-radius: 14px;
+  background: #ffffff;
+  border: 1px solid #dbe6ff;
+
+  span {
+    display: block;
+    color: #64748b;
+    margin-bottom: 8px;
+  }
+
+  strong {
+    color: #1d4ed8;
+    font-size: 18px;
+    line-height: 1.5;
+  }
+}
+
+.metric-grid {
+  display: grid;
+  grid-template-columns: repeat(4, minmax(0, 1fr));
+  gap: 14px;
+  margin-bottom: 18px;
+}
+
+.metric-card {
+  background: #fff;
+  border-radius: 10px;
+  padding: 18px 20px;
+  border-top: 4px solid #2563eb;
+
+  span,
+  em {
+    display: block;
+    color: #64748b;
+    font-style: normal;
+  }
+
+  strong {
+    display: block;
+    margin: 8px 0;
+    font-size: 24px;
+    color: #0f172a;
+  }
+
+  &.green { border-top-color: #10b981; }
+  &.violet { border-top-color: #7c3aed; }
+  &.orange { border-top-color: #f59e0b; }
+}
+
+.content-grid {
+  display: grid;
+  grid-template-columns: 1fr 1fr;
+  gap: 16px;
+  margin-bottom: 18px;
+}
+
+.box-card {
+  border-radius: 14px;
+  border: none;
+}
+
+.section-title {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  font-weight: 700;
+  color: #0f172a;
+}
+
+.formula-stack {
+  display: grid;
+  gap: 10px;
+
+  div {
+    padding: 12px 14px;
+    border-radius: 8px;
+    background: #f6f9ff;
+    color: #1d4ed8;
+    font-weight: 700;
+  }
+}
+
+.flow-line {
+  display: flex;
+  align-items: center;
+  gap: 10px;
+  flex-wrap: wrap;
+  margin-bottom: 14px;
+
+  span {
+    padding: 10px 14px;
+    border-radius: 8px;
+    background: #eef6ff;
+    color: #1f4e79;
+    font-weight: 700;
+  }
+}
+
+.muted {
+  margin: 0;
+  color: #64748b;
+  line-height: 1.8;
+}
+
+.large {
+  font-size: 15px;
+}
+
+.bottom-grid {
+  margin-top: 18px;
+}
+
+.rule-list {
+  margin: 0;
+  padding-left: 18px;
+  color: #475569;
+  line-height: 2;
+}
+
+::v-deep .el-card__header {
+  background: #fbfdff;
+  border-bottom: 1px solid #eef2f7;
+}
+
+@media (max-width: 1100px) {
+  .hero-panel,
+  .content-grid {
+    grid-template-columns: 1fr;
+    display: grid;
+  }
+
+  .metric-grid {
+    grid-template-columns: repeat(2, minmax(0, 1fr));
+  }
+}
+</style>

+ 252 - 325
src/views/supply/weights/index.vue

@@ -1,392 +1,319 @@
 <template>
-  <div class="app-container">
+  <div class="app-container supply-dashboard-page">
     <div class="page-header">
-      <h2><i class="el-icon-setting"></i> 权重配置管理</h2>
-      <p class="page-desc">自定义成本、交付、账期三个维度的评估权重</p>
+      <h2><i class="el-icon-setting"></i> &#x6743;&#x91CD;&#x914D;&#x7F6E;&#x7BA1;&#x7406;</h2>
+      <p class="page-desc">&#x8C03;&#x6574;&#x4F9B;&#x5E94;&#x76D1;&#x6D4B;&#x8BC4;&#x5206;&#x5361;&#x7684;&#x7EFC;&#x5408;&#x8BC4;&#x5206;&#x6743;&#x91CD;&#x548C;&#x8BC4;&#x5206;&#x89C4;&#x5219;&#x53C2;&#x6570;</p>
     </div>
 
     <el-card class="box-card">
       <div slot="header" class="clearfix">
-        <span><i class="el-icon-edit-outline"></i> 权重配置</span>
+        <span><i class="el-icon-edit-outline"></i> &#x8BC4;&#x5206;&#x5361;&#x6743;&#x91CD;&#x914D;&#x7F6E;</span>
       </div>
 
-      <el-form
-        :model="weights"
-        :rules="rules"
-        ref="weightsForm"
-        label-width="120px"
-      >
-        <el-form-item label="成本权重" prop="成本">
-          <el-slider
-            v-model="weights.成本"
-            :step="0.01"
-            :min="0"
-            :max="1"
-            show-input
-            @change="onWeightChange"
-          />
+      <el-form :model="weights" :rules="rules" ref="weightsForm" label-width="120px">
+        <el-form-item :label="labels.costWeight" prop="cost">
+          <el-slider v-model="weights.cost" :step="0.01" :min="0" :max="1" show-input @change="onConfigChange" />
         </el-form-item>
-
-        <el-form-item label="交付权重" prop="交付">
-          <el-slider
-            v-model="weights.交付"
-            :step="0.01"
-            :min="0"
-            :max="1"
-            show-input
-            @change="onWeightChange"
-          />
+        <el-form-item :label="labels.deliveryWeight" prop="delivery">
+          <el-slider v-model="weights.delivery" :step="0.01" :min="0" :max="1" show-input @change="onConfigChange" />
         </el-form-item>
-
-        <el-form-item label="账期权重" prop="账期">
-          <el-slider
-            v-model="weights.账期"
-            :step="0.01"
-            :min="0"
-            :max="1"
-            show-input
-            @change="onWeightChange"
-          />
+        <el-form-item :label="labels.termWeight" prop="term">
+          <el-slider v-model="weights.term" :step="0.01" :min="0" :max="1" show-input @change="onConfigChange" />
         </el-form-item>
-
-        <el-form-item>
-          <div class="weight-summary">
-            <p>权重总和: {{ totalWeight.toFixed(2) }}</p>
-            <p v-if="Math.abs(totalWeight - 1) > 0.001" class="warning">
-              警告: 权重总和应为1
-            </p>
-          </div>
+        <el-form-item :label="labels.qualityWeight" prop="quality">
+          <el-slider v-model="weights.quality" :step="0.01" :min="0" :max="1" show-input @change="onConfigChange" />
         </el-form-item>
 
         <el-form-item>
-          <el-button
-            type="primary"
-            :disabled="Math.abs(totalWeight - 1) > 0.001"
-            @click="updateWeights"
-          >
-            保存权重
-          </el-button>
-          <el-button @click="resetWeights">重置</el-button>
+          <div class="weight-summary" :class="{ invalid: !isValidTotal }">
+            <p>&#x7EFC;&#x5408;&#x5206; = &#x6210;&#x672C;&#x5206; x {{ percent(weights.cost) }} + &#x4EA4;&#x4ED8;&#x5206; x {{ percent(weights.delivery) }} + &#x8D26;&#x671F;&#x5206; x {{ percent(weights.term) }} + &#x8D28;&#x91CF;&#x5206; x {{ percent(weights.quality) }}</p>
+            <p>&#x6743;&#x91CD;&#x603B;&#x548C;: {{ totalWeight.toFixed(2) }}</p>
+            <p v-if="!isValidTotal" class="warning">&#x6743;&#x91CD;&#x603B;&#x548C;&#x5FC5;&#x987B;&#x4E3A; 1</p>
+          </div>
         </el-form-item>
       </el-form>
-
-      <div
-        v-if="message"
-        class="el-alert"
-        :class="'el-alert--' + (isError ? 'error' : 'success')"
-      >
-        <div class="el-alert__content">
-          <span class="el-alert__description">{{ message }}</span>
-        </div>
-      </div>
     </el-card>
 
-    <!-- 当前权重展示 -->
     <el-card class="box-card">
       <div slot="header" class="clearfix">
-        <span><i class="el-icon-data-line"></i> 当前评估维度权重配置</span>
+        <span><i class="el-icon-data-line"></i> &#x8BC4;&#x5206;&#x89C4;&#x5219;&#x53C2;&#x6570;&#x914D;&#x7F6E;</span>
       </div>
 
-      <div class="weight-display">
-        <div class="weight-info">
-          当前配置:成本 {{ (weights.成本 * 100).toFixed(0) }}% | 交付
-          {{ (weights.交付 * 100).toFixed(0) }}% | 账期
-          {{ (weights.账期 * 100).toFixed(0) }}%
-        </div>
+      <div class="weight-info">
+        &#x5F53;&#x524D;&#x914D;&#x7F6E;&#xFF1A;&#x6210;&#x672C; {{ percent(weights.cost) }} | &#x4EA4;&#x4ED8; {{ percent(weights.delivery) }} | &#x8D26;&#x671F; {{ percent(weights.term) }} | &#x8D28;&#x91CF; {{ percent(weights.quality) }}
+      </div>
 
-        <el-row :gutter="20">
-          <el-col :span="8">
-            <div class="info-box">
-              <div class="info-label">💰 成本维度</div>
-              <div class="info-value" style="color: #5b6cff">
-                {{ (weights.成本 * 100).toFixed(0) }}%
-              </div>
-              <div class="info-detail">
-                评分规则:价格排名<br />
-                第1名: 100分<br />
-                第2名: 90分<br />
-                第3名: 80分
-              </div>
+      <el-row :gutter="20" class="rule-grid">
+        <el-col :xs="24" :sm="12" :lg="6">
+          <div class="rule-box cost-box">
+            <div class="rule-title">&#x6210;&#x672C;&#x7EF4;&#x5EA6;</div>
+            <div class="rule-desc">&#x6309;&#x4EF7;&#x683C;&#x6392;&#x540D;&#x8F6C;&#x6362;&#x4E3A;&#x6210;&#x672C;&#x5206;</div>
+            <div class="rule-field" v-for="item in costRuleItems" :key="item.key">
+              <span>{{ item.label }}</span>
+              <el-input-number v-model="scoreRules[item.key]" :min="0" :max="100" :step="1" size="mini" @change="onConfigChange" />
             </div>
-          </el-col>
-
-          <el-col :span="8">
-            <div class="info-box">
-              <div class="info-label">🚚 交付维度</div>
-              <div class="info-value" style="color: #52c41a">
-                {{ (weights.交付 * 100).toFixed(0) }}%
-              </div>
-              <div class="info-detail">
-                D1-准时率: 50%<br />
-                D2-平均偏差: 30%<br />
-                D3-最长延迟: 10%<br />
-                D4-数量满足率: 10%
-              </div>
+          </div>
+        </el-col>
+
+        <el-col :xs="24" :sm="12" :lg="6">
+          <div class="rule-box delivery-box">
+            <div class="rule-title">&#x4EA4;&#x4ED8;&#x7EF4;&#x5EA6;</div>
+            <div class="rule-desc">&#x8BC4;&#x5206;&#x5361;&#x5F53;&#x524D;&#x53EF;&#x8BA1;&#x7B97;&#x51C6;&#x65F6;&#x7387;&#x548C;&#x6570;&#x91CF;&#x6EE1;&#x8DB3;&#x7387;</div>
+            <div class="rule-field">
+              <span>D1-&#x51C6;&#x65F6;&#x7387;&#x6743;&#x91CD;</span>
+              <el-input-number v-model="scoreRules.deliveryOnTimeWeight" :min="0" :max="1" :step="0.05" size="mini" @change="onConfigChange" />
             </div>
-          </el-col>
-
-          <el-col :span="8">
-            <div class="info-box">
-              <div class="info-label">💳 账期维度</div>
-              <div class="info-value" style="color: #ff9800">
-                {{ (weights.账期 * 100).toFixed(0) }}%
-              </div>
-              <div class="info-detail">
-                ≥90天: 100分<br />
-                ≥60天: 90分<br />
-                ≥45天: 80分<br />
-                ≥30天: 60分
-              </div>
+            <div class="rule-field">
+              <span>D4-&#x6570;&#x91CF;&#x6EE1;&#x8DB3;&#x7387;&#x6743;&#x91CD;</span>
+              <el-input-number v-model="scoreRules.deliveryFulfillmentWeight" :min="0" :max="1" :step="0.05" size="mini" @change="onConfigChange" />
+            </div>
+            <div class="sub-summary" :class="{ invalid: !isValidDeliveryRules }">
+              &#x4EA4;&#x4ED8;&#x5B50;&#x9879;&#x603B;&#x548C;&#xFF1A;{{ deliveryRuleTotal.toFixed(2) }}
+            </div>
+          </div>
+        </el-col>
+
+        <el-col :xs="24" :sm="12" :lg="6">
+          <div class="rule-box term-box">
+            <div class="rule-title">&#x8D26;&#x671F;&#x7EF4;&#x5EA6;</div>
+            <div class="rule-desc">&#x8D26;&#x671F;&#x5929;&#x6570;&#x8FBE;&#x5230;&#x9608;&#x503C;&#x540E;&#x83B7;&#x5F97;&#x5BF9;&#x5E94;&#x5206;&#x6570;</div>
+            <div class="term-row" v-for="item in termRuleItems" :key="item.daysKey">
+              <el-input-number v-model="scoreRules[item.daysKey]" :min="0" :step="5" size="mini" @change="onConfigChange" />
+              <span>&#x5929;&#xFF1A;</span>
+              <el-input-number v-model="scoreRules[item.scoreKey]" :min="0" :max="100" :step="5" size="mini" @change="onConfigChange" />
+              <span>&#x5206;</span>
             </div>
-          </el-col>
-        </el-row>
+            <div class="term-row">
+              <span>&lt; &#x6700;&#x4F4E;&#x9608;&#x503C;&#xFF1A;</span>
+              <el-input-number v-model="scoreRules.termDefaultScore" :min="0" :max="100" :step="5" size="mini" @change="onConfigChange" />
+              <span>&#x5206;</span>
+            </div>
+          </div>
+        </el-col>
+
+        <el-col :xs="24" :sm="12" :lg="6">
+          <div class="rule-box quality-box">
+            <div class="rule-title">&#x8D28;&#x91CF;&#x7EF4;&#x5EA6;</div>
+            <div class="rule-desc">QA&#x5408;&#x683C;&#x7387;&#x8FDB;&#x5165;&#x8D28;&#x91CF;&#x5206;&#xFF0C;&#x53EF;&#x8C03;&#x9884;&#x8B66;&#x7EBF;&#x548C;&#x7F3A;&#x7701;&#x5206;</div>
+            <div class="rule-field">
+              <span>QA&#x9884;&#x8B66;&#x7EBF;</span>
+              <el-input-number v-model="scoreRules.qualityWarningRate" :min="0" :max="1" :step="0.01" size="mini" @change="onConfigChange" />
+            </div>
+            <div class="rule-field">
+              <span>&#x65E0;QA&#x6570;&#x636E;&#x9ED8;&#x8BA4;&#x5206;</span>
+              <el-input-number v-model="scoreRules.qualityDefaultScore" :min="0" :max="100" :step="5" size="mini" @change="onConfigChange" />
+            </div>
+            <div class="formula-line">&#x8D28;&#x91CF;&#x5206; = QA&#x5408;&#x683C;&#x7387; x 100</div>
+            <div class="formula-line">QA&#x5408;&#x683C;&#x7387; &lt; {{ percent(scoreRules.qualityWarningRate) }} &#x89E6;&#x53D1;&#x8D28;&#x91CF;&#x9884;&#x8B66;</div>
+          </div>
+        </el-col>
+      </el-row>
+
+      <div class="action-row">
+        <el-button type="primary" :disabled="!canSave || loading" @click="updateWeights">&#x4FDD;&#x5B58;&#x914D;&#x7F6E;</el-button>
+        <el-button @click="resetWeights">&#x6062;&#x590D;&#x9ED8;&#x8BA4;</el-button>
+      </div>
+
+      <div v-if="message" class="el-alert" :class="'el-alert--' + (isError ? 'error' : 'success')">
+        <div class="el-alert__content"><span class="el-alert__description">{{ message }}</span></div>
       </div>
     </el-card>
   </div>
 </template>
 
 <script>
-import { getWeightsConfig, updateWeightsConfig } from "@/api/supply";
+import { getWeightsConfig, updateWeightsConfig } from '@/api/supply'
+
+const defaultWeights = { cost: 0.30, delivery: 0.30, term: 0.15, quality: 0.25 }
+const defaultScoreRules = {
+  costRank1Score: 100,
+  costRank2Score: 90,
+  costRank3Score: 80,
+  costRank4Score: 60,
+  deliveryOnTimeWeight: 0.70,
+  deliveryFulfillmentWeight: 0.30,
+  termExcellentDays: 90,
+  termGoodDays: 60,
+  termNormalDays: 45,
+  termShortDays: 30,
+  termExcellentScore: 100,
+  termGoodScore: 90,
+  termNormalScore: 80,
+  termShortScore: 60,
+  termDefaultScore: 40,
+  qualityWarningRate: 0.95,
+  qualityDefaultScore: 80
+}
 
 export default {
-  name: "SupplyWeightsConfig",
+  name: 'SupplyWeightsConfig',
   data() {
+    const rule = [
+      { required: true, message: '\u8bf7\u8f93\u5165\u6743\u91cd', trigger: 'blur' },
+      { type: 'number', min: 0, max: 1, message: '\u6743\u91cd\u5e94\u57280\u52301\u4e4b\u95f4', trigger: 'blur' }
+    ]
     return {
-      weights: {
-        成本: 0.4,
-        交付: 0.4,
-        账期: 0.2,
+      weights: { ...defaultWeights },
+      scoreRules: { ...defaultScoreRules },
+      labels: {
+        costWeight: '\u6210\u672c\u6743\u91cd',
+        deliveryWeight: '\u4ea4\u4ed8\u6743\u91cd',
+        termWeight: '\u8d26\u671f\u6743\u91cd',
+        qualityWeight: '\u8d28\u91cf\u6743\u91cd'
       },
+      costRuleItems: [
+        { key: 'costRank1Score', label: '\u7b2c1\u540d' },
+        { key: 'costRank2Score', label: '\u7b2c2\u540d' },
+        { key: 'costRank3Score', label: '\u7b2c3\u540d' },
+        { key: 'costRank4Score', label: '\u7b2c4\u540d' }
+      ],
+      termRuleItems: [
+        { daysKey: 'termExcellentDays', scoreKey: 'termExcellentScore' },
+        { daysKey: 'termGoodDays', scoreKey: 'termGoodScore' },
+        { daysKey: 'termNormalDays', scoreKey: 'termNormalScore' },
+        { daysKey: 'termShortDays', scoreKey: 'termShortScore' }
+      ],
       loading: false,
-      message: "",
+      message: '',
       isError: false,
-      rules: {
-        成本: [
-          { required: true, message: "请输入成本权重", trigger: "blur" },
-          {
-            type: "number",
-            min: 0,
-            max: 1,
-            message: "权重应在0到1之间",
-            trigger: "blur",
-          },
-        ],
-        交付: [
-          { required: true, message: "请输入交付权重", trigger: "blur" },
-          {
-            type: "number",
-            min: 0,
-            max: 1,
-            message: "权重应在0到1之间",
-            trigger: "blur",
-          },
-        ],
-        账期: [
-          { required: true, message: "请输入账期权重", trigger: "blur" },
-          {
-            type: "number",
-            min: 0,
-            max: 1,
-            message: "权重应在0到1之间",
-            trigger: "blur",
-          },
-        ],
-      },
-    };
+      rules: { cost: rule, delivery: rule, term: rule, quality: rule }
+    }
   },
   computed: {
-    // 计算权重总和
     totalWeight() {
-      return this.weights.成本 + this.weights.交付 + this.weights.账期;
+      return this.weights.cost + this.weights.delivery + this.weights.term + this.weights.quality
+    },
+    isValidTotal() {
+      return Math.abs(this.totalWeight - 1) <= 0.001
+    },
+    deliveryRuleTotal() {
+      return this.scoreRules.deliveryOnTimeWeight + this.scoreRules.deliveryFulfillmentWeight
     },
+    isValidDeliveryRules() {
+      return Math.abs(this.deliveryRuleTotal - 1) <= 0.001
+    },
+    canSave() {
+      return this.isValidTotal && this.isValidDeliveryRules
+    }
   },
   created() {
-    this.fetchWeights();
+    this.fetchWeights()
   },
   methods: {
-    // 获取当前权重配置
+    normalizeWeights(data) {
+      const source = data && data.data ? data.data : data
+      const weightsSource = source && source.weights ? source.weights : source
+      return {
+        cost: this.pickNumber(weightsSource, defaultWeights.cost, ['\u6210\u672c', 'cost_weight', 'costWeight', '\u6210\u672c\u6743\u91cd']),
+        delivery: this.pickNumber(weightsSource, defaultWeights.delivery, ['\u4ea4\u4ed8', 'delivery_weight', 'deliveryWeight', '\u4ea4\u4ed8\u6743\u91cd']),
+        term: this.pickNumber(weightsSource, defaultWeights.term, ['\u8d26\u671f', 'term_weight', 'termWeight', 'payment_weight', '\u8d26\u671f\u6743\u91cd']),
+        quality: this.pickNumber(weightsSource, defaultWeights.quality, ['\u8d28\u91cf', 'quality_weight', 'qualityWeight', '\u8d28\u91cf\u6743\u91cd'])
+      }
+    },
+    normalizeRules(data) {
+      const source = data && data.data ? data.data : data
+      const rulesSource = source && source.rules ? source.rules : {}
+      const next = { ...defaultScoreRules }
+      Object.keys(next).forEach(key => {
+        next[key] = this.pickNumber(rulesSource, next[key], [key])
+      })
+      return next
+    },
+    pickNumber(source, fallback, keys) {
+      if (!source) return fallback
+      for (let i = 0; i < keys.length; i++) {
+        const value = source[keys[i]]
+        if (value !== undefined && value !== null && value !== '') {
+          return Number(value)
+        }
+      }
+      return fallback
+    },
+    toPayload() {
+      return {
+        '\u6210\u672c': this.weights.cost,
+        '\u4ea4\u4ed8': this.weights.delivery,
+        '\u8d26\u671f': this.weights.term,
+        '\u8d28\u91cf': this.weights.quality,
+        costWeight: this.weights.cost,
+        deliveryWeight: this.weights.delivery,
+        termWeight: this.weights.term,
+        qualityWeight: this.weights.quality,
+        rules: { ...this.scoreRules }
+      }
+    },
     async fetchWeights() {
-      this.loading = true;
+      this.loading = true
       try {
-        const response = await getWeightsConfig();
-        // Flask API可能直接返回权重对象,而不是封装在data字段中
-        if (
-          response &&
-          typeof response === "object" &&
-          !Array.isArray(response)
-        ) {
-          this.weights = {
-            成本:
-              response.成本 || response.成本权重 || response.cost_weight || 0.4,
-            交付:
-              response.交付 ||
-              response.交付权重 ||
-              response.delivery_weight ||
-              0.4,
-            账期:
-              response.账期 ||
-              response.账期权重 ||
-              response.payment_weight ||
-              0.2,
-          };
-        } else {
-          this.weights = {
-            成本: 0.4,
-            交付: 0.4,
-            账期: 0.2,
-          };
-        }
-        this.$modal.msgSuccess("权重配置加载成功");
+        const response = await getWeightsConfig()
+        this.weights = this.normalizeWeights(response)
+        this.scoreRules = this.normalizeRules(response)
+        this.message = ''
       } catch (err) {
-        console.error("获取权重配置失败:", err);
-        this.message = "获取权重配置失败";
-        this.isError = true;
-        this.$modal.msgError(this.message);
+        console.error(err)
+        this.weights = { ...defaultWeights }
+        this.scoreRules = { ...defaultScoreRules }
+        this.message = '\u6743\u91cd\u63a5\u53e3\u6682\u65f6\u4e0d\u53ef\u7528\uff0c\u5df2\u4f7f\u7528\u9ed8\u8ba4\u914d\u7f6e'
+        this.isError = false
       } finally {
-        this.loading = false;
+        this.loading = false
       }
     },
-
-    // 更新权重配置
     async updateWeights() {
-      if (Math.abs(this.totalWeight - 1) > 0.001) {
-        this.message = "权重总和必须为1";
-        this.isError = true;
-        this.$modal.msgError(this.message);
-        return;
+      if (!this.canSave) {
+        this.message = !this.isValidTotal ? '\u6743\u91cd\u603b\u548c\u5fc5\u987b\u4e3a1' : '\u4ea4\u4ed8\u5b50\u9879\u6743\u91cd\u603b\u548c\u5fc5\u987b\u4e3a1'
+        this.isError = true
+        this.$modal.msgError(this.message)
+        return
       }
-
       try {
-        const response = await updateWeightsConfig(this.weights);
-        // Flask API可能直接返回成功消息,而不是封装在msg字段中
-        if (response && response.message) {
-          this.message = response.message;
-        } else if (response && response.msg) {
-          this.message = response.msg;
-        } else {
-          this.message = "权重更新成功";
-        }
-        this.isError = false;
-        this.$modal.msgSuccess(this.message);
+        const response = await updateWeightsConfig(this.toPayload())
+        this.weights = this.normalizeWeights(response)
+        this.scoreRules = this.normalizeRules(response)
+        this.message = '\u914d\u7f6e\u66f4\u65b0\u6210\u529f'
+        this.isError = false
+        this.$modal.msgSuccess(this.message)
       } catch (err) {
-        console.error("更新权重配置失败:", err);
-        this.message = err.message || "更新权重配置失败";
-        this.isError = true;
-        this.$modal.msgError(this.message);
+        console.error(err)
+        this.message = err.message || '\u914d\u7f6e\u66f4\u65b0\u5931\u8d25'
+        this.isError = true
+        this.$modal.msgError(this.message)
       }
     },
-
-    // 重置权重
     resetWeights() {
-      this.weights = {
-        成本: 0.4,
-        交付: 0.4,
-        账期: 0.2,
-      };
-      this.$modal.msgInfo("权重已重置为默认值");
+      this.weights = { ...defaultWeights }
+      this.scoreRules = { ...defaultScoreRules }
+      this.message = ''
+      this.isError = false
     },
-
-    // 权重变更事件
-    onWeightChange() {
-      this.message = "";
-      this.isError = false;
+    onConfigChange() {
+      this.message = ''
+      this.isError = false
     },
-  },
-};
-</script>
-
-<style lang="scss" scoped>
-.app-container {
-  padding: 20px;
-}
-
-.page-header {
-  margin-bottom: 20px;
-
-  h2 {
-    font-size: 24px;
-    font-weight: 600;
-    color: #303133;
-    margin-bottom: 8px;
-
-    i {
-      margin-right: 8px;
-      color: #409eff;
+    percent(value) {
+      return `${(Number(value || 0) * 100).toFixed(0)}%`
     }
   }
-
-  .page-desc {
-    color: #909399;
-    font-size: 14px;
-    margin: 0;
-  }
-}
-
-.box-card {
-  margin-bottom: 20px;
-}
-
-.weight-summary {
-  margin: 20px 0;
-  padding: 15px;
-  background: #f8f9fa;
-  border-radius: 4px;
-  border-left: 4px solid #409eff;
-
-  p {
-    margin: 0 0 5px 0;
-    font-size: 14px;
-    color: #606266;
-  }
-}
-
-.warning {
-  color: #e6a23c;
-  font-weight: 600;
-}
-
-.weight-display {
-  .weight-info {
-    margin-bottom: 20px;
-    color: #666;
-    font-size: 14px;
-  }
-}
-
-.info-box {
-  background: #f8f9fa;
-  padding: 20px;
-  border-radius: 4px;
-  border: 1px solid #ebeef5;
-  transition: all 0.3s;
-
-  &:hover {
-    transform: translateY(-2px);
-    box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
-  }
-
-  .info-label {
-    font-size: 12px;
-    color: #909399;
-    margin-bottom: 10px;
-    font-weight: 600;
-  }
-
-  .info-value {
-    font-size: 24px;
-    font-weight: 700;
-    margin-bottom: 12px;
-    color: #303133;
-  }
-
-  .info-detail {
-    font-size: 13px;
-    color: #909399;
-    line-height: 1.8;
-  }
 }
+</script>
 
-::v-deep .el-card__header {
-  font-weight: bold;
-}
+<style lang="scss" scoped>
+.app-container { padding: 20px; }
+.page-header { margin-bottom: 20px; h2 { font-size: 24px; font-weight: 600; color: #303133; margin-bottom: 8px; i { margin-right: 8px; color: #409eff; } } .page-desc { color: #909399; font-size: 14px; margin: 0; } }
+.box-card { margin-bottom: 20px; }
+.weight-summary { margin: 20px 0; padding: 15px; background: #f8f9fa; border-radius: 4px; border-left: 4px solid #409eff; p { margin: 0 0 5px 0; font-size: 14px; color: #606266; } &.invalid { border-left-color: #f56c6c; } .warning { color: #f56c6c; font-weight: 500; } }
+.weight-info { margin-bottom: 18px; color: #606266; font-size: 14px; }
+.rule-grid { margin-bottom: 18px; }
+.rule-box { padding: 18px 20px; background: #f8f9fb; border-radius: 8px; min-height: 278px; border: 1px solid #edf1f7; }
+.rule-title { color: #001a45; font-size: 15px; font-weight: 600; margin-bottom: 8px; }
+.rule-desc { color: #7d8aa6; font-size: 12px; line-height: 1.5; min-height: 36px; margin-bottom: 12px; }
+.rule-field { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: 10px; color: #59667f; font-size: 13px; }
+.term-row { display: flex; align-items: center; gap: 6px; margin-bottom: 8px; color: #59667f; font-size: 13px; }
+.term-row .el-input-number { width: 84px; }
+.rule-field .el-input-number { width: 112px; }
+.sub-summary { margin-top: 12px; padding: 8px 10px; border-radius: 6px; background: #eef6ff; color: #2468d6; font-size: 13px; }
+.sub-summary.invalid { background: #fff1f0; color: #f56c6c; }
+.formula-line { color: #7d8aa6; font-size: 13px; line-height: 1.7; }
+.action-row { margin-top: 8px; }
+.el-alert { margin-top: 15px; padding: 10px 16px; border-radius: 4px; }
 </style>

+ 1 - 0
vue.config.js

@@ -35,6 +35,7 @@ module.exports = {
     host: '0.0.0.0',
     port: port,
     open: false,
+    historyApiFallback: true,
     proxy: {
       // detail: https://cli.vuejs.org/config/#devserver-proxy
       '^/api': {

Неке датотеке нису приказане због велике количине промена