m0_70156489 1 週間 前
コミット
f43c20a916

+ 12 - 59
application/admin/controller/Dashboard.php

@@ -2,18 +2,14 @@
 
 namespace app\admin\controller;
 
-use app\admin\model\Admin;
-use app\admin\model\User;
 use app\common\controller\Backend;
-use app\common\model\Attachment;
-use fast\Date;
-use think\Db;
+use app\common\library\ProcuremenDashboard;
 
 /**
  * 控制台
  *
  * @icon   fa fa-dashboard
- * @remark 用于展示当前系统中的统计数据、统计报表及重要实时数据
+ * @remark 协助采购数据概览
  */
 class Dashboard extends Backend
 {
@@ -23,61 +19,18 @@ class Dashboard extends Backend
      */
     public function index()
     {
-        try {
-            \think\Db::execute("SET @@sql_mode='';");
-        } catch (\Exception $e) {
-
-        }
-        $column = [];
-        $starttime = Date::unixtime('day', -6);
-        $endtime = Date::unixtime('day', 0, 'end');
-        $joinlist = Db("user")->where('jointime', 'between time', [$starttime, $endtime])
-            ->field('jointime, status, COUNT(*) AS nums, DATE_FORMAT(FROM_UNIXTIME(jointime), "%Y-%m-%d") AS join_date')
-            ->group('join_date')
-            ->select();
-        for ($time = $starttime; $time <= $endtime;) {
-            $column[] = date("Y-m-d", $time);
-            $time += 86400;
-        }
-        $userlist = array_fill_keys($column, 0);
-        foreach ($joinlist as $k => $v) {
-            $userlist[$v['join_date']] = $v['nums'];
-        }
-
-        $dbTableList = Db::query("SHOW TABLE STATUS");
-        $addonList = get_addon_list();
-        $totalworkingaddon = 0;
-        $totaladdon = count($addonList);
-        foreach ($addonList as $index => $item) {
-            if ($item['state']) {
-                $totalworkingaddon += 1;
-            }
-        }
-        $this->view->assign([
-            'totaluser'         => User::count(),
-            'totaladdon'        => $totaladdon,
-            'totaladmin'        => Admin::count(),
-            'totalcategory'     => \app\common\model\Category::count(),
-            'todayusersignup'   => User::whereTime('jointime', 'today')->count(),
-            'todayuserlogin'    => User::whereTime('logintime', 'today')->count(),
-            'sevendau'          => User::whereTime('jointime|logintime|prevtime', '-7 days')->count(),
-            'thirtydau'         => User::whereTime('jointime|logintime|prevtime', '-30 days')->count(),
-            'threednu'          => User::whereTime('jointime', '-3 days')->count(),
-            'sevendnu'          => User::whereTime('jointime', '-7 days')->count(),
-            'dbtablenums'       => count($dbTableList),
-            'dbsize'            => array_sum(array_map(function ($item) {
-                return $item['Data_length'] + $item['Index_length'];
-            }, $dbTableList)),
-            'totalworkingaddon' => $totalworkingaddon,
-            'attachmentnums'    => Attachment::count(),
-            'attachmentsize'    => Attachment::sum('filesize'),
-            'picturenums'       => Attachment::where('mimetype', 'like', 'image/%')->count(),
-            'picturesize'       => Attachment::where('mimetype', 'like', 'image/%')->sum('filesize'),
+        $procStats = ProcuremenDashboard::buildStats();
+        $this->view->assign('procuremenStats', $procStats);
+
+        $last7 = $procStats['last7'] ?? [];
+        $this->assignconfig('procuremenChart', [
+            'labels'    => $last7['labels'] ?? [],
+            'issued'    => $last7['issued'] ?? [],
+            'confirm'   => $last7['confirm'] ?? [],
+            'approval'  => $last7['approval'] ?? [],
+            'completed' => $last7['completed'] ?? [],
         ]);
 
-        $this->assignconfig('column', array_keys($userlist));
-        $this->assignconfig('userdata', array_values($userlist));
-
         return $this->view->fetch();
     }
 

+ 2 - 0
application/admin/view/common/header.html

@@ -4,6 +4,8 @@
     <span class="logo-mini">{$site.name|mb_substr=0,4,'utf-8'|mb_strtoupper='utf-8'|htmlentities}</span>
     <!-- 普通模式下Logo -->
     <span class="logo-lg" style=" font-size: 16px;">{$site.name|htmlentities}</span>
+    <!-- <span class="logo-lg" style=" font-size: 16px;">大数据协同系统</span> -->
+
 </a>
 
 <!-- 顶部通栏样式 -->

+ 621 - 212
application/admin/view/dashboard/index.html

@@ -158,246 +158,655 @@
     #statistics .panel h5 {
         font-size: 14px;
     }
+
+    html, body {
+        background: #fff;
+    }
+
+    .panel-intro {
+        margin-bottom: 0;
+        background: #fff;
+    }
+
+    .proc-dash {
+        margin-bottom: 0;
+        padding-top: 12px;
+    }
+
+    .proc-kpi-section {
+        margin-bottom: 14px;
+        flex-shrink: 0;
+    }
+
+    .proc-kpi-section:last-of-type {
+        margin-bottom: 16px;
+    }
+
+    .proc-kpi-section-title {
+        font-size: 14px;
+        font-weight: 600;
+        color: #555;
+        margin: 0 0 10px 2px;
+        display: flex;
+        align-items: center;
+        gap: 8px;
+    }
+
+    .proc-kpi-section-title i {
+        color: #23b7e5;
+        font-size: 13px;
+    }
+
+    .proc-dash-overview {
+        display: grid;
+        grid-template-columns: repeat(4, minmax(0, 1fr));
+        gap: 12px;
+        margin: 0;
+    }
+
+    .proc-dash-overview.cols-3 {
+        grid-template-columns: repeat(3, minmax(0, 1fr));
+    }
+
+    @media (max-width: 991px) {
+        .proc-dash-overview,
+        .proc-dash-overview.cols-3 {
+            grid-template-columns: repeat(2, minmax(0, 1fr));
+        }
+    }
+
+    @media (max-width: 479px) {
+        .proc-dash-overview,
+        .proc-dash-overview.cols-3 {
+            grid-template-columns: 1fr;
+        }
+    }
+
+    .proc-overview-item {
+        background: #fff;
+        border-radius: 8px;
+        padding: 14px 16px;
+        display: flex;
+        align-items: center;
+        gap: 12px;
+        box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
+        min-height: 78px;
+    }
+
+    .proc-overview-item .overview-icon {
+        flex: 0 0 40px;
+        width: 40px;
+        height: 40px;
+        line-height: 40px;
+        text-align: center;
+        border-radius: 8px;
+        font-size: 18px;
+        color: #fff;
+    }
+
+    .proc-overview-item.item-supplier .overview-icon { background: #23b7e5; }
+    .proc-overview-item.item-confirm .overview-icon { background: #f0ad4e; }
+    .proc-overview-item.item-approval .overview-icon { background: #7266ba; }
+    .proc-overview-item.item-completed .overview-icon { background: #27C24C; }
+
+    .proc-overview-item .overview-body {
+        min-width: 0;
+        flex: 1;
+    }
+
+    .proc-overview-item .overview-title {
+        font-size: 13px;
+        font-weight: 600;
+        color: #444;
+        margin-bottom: 6px;
+    }
+
+    .proc-overview-item .overview-metrics {
+        display: flex;
+        align-items: baseline;
+        gap: 18px;
+    }
+
+    .proc-overview-item .overview-metric {
+        display: flex;
+        align-items: baseline;
+        gap: 4px;
+        min-width: 0;
+    }
+
+    .proc-overview-item .overview-num {
+        font-size: 22px;
+        line-height: 1.2;
+        font-weight: 700;
+        color: #222;
+    }
+
+    .proc-overview-item.item-confirm .overview-num { color: #f0ad4e; }
+    .proc-overview-item.item-approval .overview-num { color: #7266ba; }
+    .proc-overview-item.item-completed .overview-num { color: #27C24C; }
+
+    .proc-overview-item.item-confirm .overview-num-single { color: #f0ad4e; }
+    .proc-overview-item.item-approval .overview-num-single { color: #7266ba; }
+    .proc-overview-item.item-completed .overview-num-single { color: #27C24C; }
+
+    .proc-overview-item .overview-num-single {
+        font-size: 26px;
+        line-height: 1.2;
+        font-weight: 700;
+        color: #222;
+    }
+
+    .proc-overview-item .overview-metric-label {
+        font-size: 12px;
+        color: #999;
+        white-space: nowrap;
+    }
+
+    .proc-overview-item .overview-link {
+        display: block;
+        margin-top: 6px;
+        font-size: 12px;
+    }
+
+    .proc-overview-item.item-confirm .overview-link {
+        color: #f0ad4e;
+    }
+
+    .proc-overview-item.item-approval .overview-link {
+        color: #7266ba;
+    }
+
+    .proc-overview-item.item-completed .overview-link {
+        color: #27C24C;
+    }
+
+    .proc-overview-item.item-confirm .overview-link:hover {
+        color: #ec971f;
+    }
+
+    .proc-overview-item.item-approval .overview-link:hover {
+        color: #6254b2;
+    }
+
+    .proc-overview-item.item-completed .overview-link:hover {
+        color: #1e983e;
+    }
+
+    .proc-dash-pending {
+        display: flex;
+        flex-wrap: wrap;
+        gap: 10px;
+        margin-bottom: 14px;
+    }
+
+    .proc-dash-pending .label {
+        font-size: 13px;
+        padding: 7px 12px;
+        border-radius: 20px;
+    }
+
+    .proc-stage-cards-row {
+        display: grid;
+        grid-template-columns: repeat(3, minmax(0, 1fr));
+        gap: 12px;
+        margin: 0 0 20px;
+    }
+
+    @media (max-width: 991px) {
+        .proc-stage-cards-row {
+            grid-template-columns: repeat(2, minmax(0, 1fr));
+        }
+    }
+
+    @media (max-width: 479px) {
+        .proc-stage-cards-row {
+            grid-template-columns: 1fr;
+        }
+    }
+
+    .proc-stage-cards-row > .proc-stage-col {
+        min-width: 0;
+    }
+
+    .proc-stage-card {
+        background: #fff;
+        border-radius: 8px;
+        padding: 14px 12px 12px;
+        margin-bottom: 0;
+        box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
+        border-top: 4px solid #ddd;
+        height: 100%;
+        display: flex;
+        flex-direction: column;
+        transition: box-shadow .2s ease;
+    }
+
+    .proc-stage-card:hover {
+        box-shadow: 0 4px 14px rgba(0, 0, 0, 0.08);
+    }
+
+    .proc-stage-card.stage-issued { border-top-color: #23b7e5; }
+    .proc-stage-card.stage-confirm { border-top-color: #f0ad4e; }
+    .proc-stage-card.stage-approval { border-top-color: #7266ba; }
+    .proc-stage-card.stage-completed { border-top-color: #27C24C; }
+
+    .proc-stage-card .stage-title {
+        font-size: 15px;
+        font-weight: 600;
+        color: #333;
+        margin-bottom: 4px;
+    }
+
+    .proc-stage-card .stage-desc {
+        font-size: 12px;
+        color: #999;
+        margin-bottom: 12px;
+        min-height: 32px;
+        line-height: 1.4;
+    }
+
+    .proc-stage-card .stage-metrics {
+        display: grid;
+        grid-template-columns: 1fr 1fr;
+        gap: 0;
+        border-top: 1px solid #f0f0f0;
+        padding-top: 12px;
+        margin-top: auto;
+    }
+
+    .proc-stage-card .metric-block {
+        text-align: center;
+        padding: 0 6px;
+    }
+
+    .proc-stage-card .metric-block + .metric-block {
+        border-left: 1px solid #f0f0f0;
+    }
+
+    .proc-stage-card .metric-block .metric-num {
+        font-size: 26px;
+        line-height: 1.2;
+        font-weight: 700;
+        color: #222;
+        min-height: 32px;
+    }
+
+    .proc-stage-card .metric-block .metric-label {
+        font-size: 12px;
+        color: #888;
+        margin-top: 4px;
+        white-space: nowrap;
+    }
+
+    .proc-stage-card .metric-block.metric-week .metric-num {
+        color: #7266ba;
+    }
+
+    .proc-stage-card.stage-issued .metric-block .metric-num { color: #23b7e5; }
+    .proc-stage-card.stage-confirm .metric-block .metric-num { color: #f0ad4e; }
+    .proc-stage-card.stage-approval .metric-block .metric-num { color: #7266ba; }
+    .proc-stage-card.stage-completed .metric-block .metric-num { color: #27C24C; }
+
+    .proc-stage-card .stage-link {
+        display: inline-block;
+        margin-top: 12px;
+        font-size: 12px;
+    }
+
+    .proc-dash-table th,
+    .proc-dash-table td {
+        text-align: center !important;
+        vertical-align: middle !important;
+    }
+
+    .proc-dash-table th:first-child,
+    .proc-dash-table td:first-child {
+        text-align: left !important;
+    }
+
+    .proc-dash-table td strong {
+        font-size: 16px;
+    }
+
+    .proc-dash-chart-row {
+        margin-top: 0;
+        display: grid;
+        grid-template-columns: minmax(0, 1fr) minmax(340px, 32%);
+        gap: 12px;
+        align-items: stretch;
+        height: 300px;
+    }
+
+    .proc-dash-chart-row .chart-col,
+    .proc-dash-chart-row .side-col {
+        min-width: 0;
+        height: 100%;
+        overflow: visible;
+    }
+
+    .proc-dash-chart-row #procuremen-echart {
+        height: 100%;
+        min-height: 0;
+        width: 100%;
+        background: #fff;
+        border-radius: 4px;
+        box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
+        box-sizing: border-box;
+        padding: 0 4px 2px 0;
+    }
+
+    .proc-summary-panel {
+        background: #fff;
+        border-radius: 4px;
+        padding: 12px 14px;
+        width: 100%;
+        height: 100%;
+        box-sizing: border-box;
+        box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
+        display: flex;
+        flex-direction: column;
+        overflow: hidden;
+    }
+
+    @media (max-width: 991px) {
+        .proc-dash-chart-row {
+            grid-template-columns: 1fr;
+            height: auto;
+        }
+
+        .proc-dash-chart-row .chart-col {
+            height: 260px;
+        }
+
+        .proc-dash-chart-row .side-col {
+            height: auto;
+        }
+
+        .proc-dash-chart-row .side-col .proc-summary-panel {
+            height: auto;
+            overflow: visible;
+        }
+    }
+
+    .panel-intro > .panel-body {
+        padding: 15px 15px 12px;
+        box-sizing: border-box;
+    }
+
+    .proc-rank-panel .rank-head {
+        display: flex;
+        align-items: center;
+        gap: 8px;
+        margin-bottom: 10px;
+        flex-shrink: 0;
+        flex-wrap: wrap;
+    }
+
+    .proc-rank-panel .rank-title {
+        font-size: 13px;
+        font-weight: 600;
+        color: #444;
+        flex: 1;
+        min-width: 0;
+    }
+
+    .proc-rank-panel .rank-supplier-inline {
+        font-size: 12px;
+        color: #666;
+        white-space: nowrap;
+    }
+
+    .proc-rank-panel .rank-supplier-inline strong {
+        font-size: 16px;
+        color: #23b7e5;
+        margin-left: 2px;
+    }
+
+    .proc-rank-panel .rank-period {
+        font-size: 11px;
+        color: #999;
+        background: #f5f5f5;
+        padding: 2px 8px;
+        border-radius: 10px;
+        white-space: nowrap;
+    }
+
+    .proc-rank-panel .rank-tabs {
+        margin-bottom: 10px;
+        border-bottom: 1px solid #eee;
+        flex-shrink: 0;
+    }
+
+    .proc-rank-panel .rank-tabs > li > a {
+        padding: 5px 8px;
+        font-size: 12px;
+        color: #888;
+        border: none !important;
+        margin-right: 0;
+    }
+
+    .proc-rank-panel .rank-tabs > li.active > a,
+    .proc-rank-panel .rank-tabs > li.active > a:hover {
+        color: #23b7e5;
+        background: transparent;
+        border-bottom: 2px solid #23b7e5 !important;
+    }
+
+    .proc-rank-panel .rank-list {
+        flex: 1;
+        min-height: 0;
+        overflow-y: auto;
+    }
+
+    .proc-rank-panel .rank-list .tab-pane {
+        padding-top: 2px;
+    }
+
+    .proc-rank-panel .rank-item {
+        display: flex;
+        align-items: flex-start;
+        gap: 10px;
+        margin-bottom: 10px;
+        padding: 8px 10px;
+        background: #fafbfc;
+        border-radius: 6px;
+    }
+
+    .proc-rank-panel .rank-item:last-child {
+        margin-bottom: 0;
+    }
+
+    .proc-rank-panel .rank-no {
+        flex: 0 0 22px;
+        width: 22px;
+        height: 22px;
+        line-height: 22px;
+        text-align: center;
+        border-radius: 6px;
+        font-size: 12px;
+        font-weight: 700;
+        color: #888;
+        background: #eee;
+        margin-top: 1px;
+    }
+
+    .proc-rank-panel .rank-no-1 { background: #ffd666; color: #874d00; }
+    .proc-rank-panel .rank-no-2 { background: #e8e8e8; color: #595959; }
+    .proc-rank-panel .rank-no-3 { background: #ffbb96; color: #873800; }
+
+    .proc-rank-panel .rank-body {
+        flex: 1;
+        min-width: 0;
+    }
+
+    .proc-rank-panel .rank-name-row {
+        display: flex;
+        align-items: center;
+        justify-content: space-between;
+        gap: 6px;
+        margin-bottom: 4px;
+    }
+
+    .proc-rank-panel .rank-name {
+        font-size: 13px;
+        color: #333;
+        white-space: nowrap;
+        overflow: hidden;
+        text-overflow: ellipsis;
+        flex: 1;
+        min-width: 0;
+    }
+
+    .proc-rank-panel .rank-value {
+        font-size: 15px;
+        font-weight: 700;
+        color: #23b7e5;
+        flex-shrink: 0;
+    }
+
+    .proc-rank-panel .rank-bar {
+        height: 5px;
+        margin: 0;
+        background: #e8e8e8;
+        border-radius: 3px;
+    }
+
+    .proc-rank-panel .rank-bar .progress-bar {
+        background: #23b7e5;
+        border-radius: 2px;
+    }
+
+    .proc-rank-panel .rank-empty {
+        text-align: center;
+        color: #bbb;
+        font-size: 13px;
+        padding: 20px 0 8px;
+    }
 </style>
+<?php $ps = isset($procuremenStats) && is_array($procuremenStats) ? $procuremenStats : ['kpi'=>[], 'today'=>[], 'week'=>[], 'pending'=>[], 'supplierRank'=>['quote'=>[], 'selected'=>[], 'period_label'=>'本周'], 'updated'=>'']; ?>
+<?php $supplierRank = isset($ps['supplierRank']) && is_array($ps['supplierRank']) ? $ps['supplierRank'] : ['quote'=>[], 'selected'=>[], 'period_label'=>'本月', 'supplier_total'=>0]; ?>
+<?php $kpi = isset($ps['kpi']) && is_array($ps['kpi']) ? $ps['kpi'] : ['supplier'=>['total'=>0],'confirm'=>['today'=>0,'month'=>0],'approval'=>['today'=>0,'month'=>0],'completed'=>['today'=>0,'month'=>0]]; ?>
 <div class="panel panel-default panel-intro">
-    <div class="panel-heading">
-        {:build_heading(null, false)}
-        <ul class="nav nav-tabs">
-            <li class="active"><a href="#one" data-toggle="tab">{:__('Dashboard')}</a></li>
-            <li><a href="#two" data-toggle="tab">{:__('Custom')}</a></li>
-        </ul>
-    </div>
     <div class="panel-body">
-        <div id="myTabContent" class="tab-content">
-            <div class="tab-pane fade active in" id="one">
-
-                <div class="row">
-                    <div class="col-sm-3 col-xs-6">
-                        <div class="sm-st clearfix">
-                            <span class="sm-st-icon st-red"><i class="fa fa-users"></i></span>
-                            <div class="sm-st-info">
-                                <span>{$totaluser}</span>
-                                {:__('Total user')}
-                            </div>
-                        </div>
-                    </div>
-                    <div class="col-sm-3 col-xs-6">
-                        <div class="sm-st clearfix">
-                            <span class="sm-st-icon st-violet"><i class="fa fa-magic"></i></span>
-                            <div class="sm-st-info">
-                                <span>{$totaladdon}</span>
-                                {:__('Total addon')}
-                            </div>
-                        </div>
-                    </div>
-                    <div class="col-sm-3 col-xs-6">
-                        <div class="sm-st clearfix">
-                            <span class="sm-st-icon st-blue"><i class="fa fa-leaf"></i></span>
-                            <div class="sm-st-info">
-                                <span>{$attachmentnums}</span>
-                                {:__('Total attachment')}
-                            </div>
-                        </div>
-                    </div>
-                    <div class="col-sm-3 col-xs-6">
-                        <div class="sm-st clearfix">
-                            <span class="sm-st-icon st-green"><i class="fa fa-user"></i></span>
-                            <div class="sm-st-info">
-                                <span>{$totaladmin}</span>
-                                {:__('Total admin')}
-                            </div>
-                        </div>
-                    </div>
-                </div>
-
-                <div class="row">
-                    <div class="col-lg-8">
-                        <div id="echart" class="btn-refresh" style="height:300px;width:100%;"></div>
-                    </div>
-                    <div class="col-lg-4">
-                        <div class="card sameheight-item stats">
-                            <div class="card-block">
-                                <div class="row row-sm stats-container">
-                                    <div class="col-xs-6 stat-col">
-                                        <div class="stat-icon"><i class="fa fa-rocket"></i></div>
-                                        <div class="stat">
-                                            <div class="value"> {$todayusersignup}</div>
-                                            <div class="name"> {:__('Today user signup')}</div>
-                                        </div>
-                                        <div class="progress">
-                                            <div class="progress-bar progress-bar-success" style="width: 20%"></div>
-                                        </div>
-                                    </div>
-                                    <div class="col-xs-6 stat-col">
-                                        <div class="stat-icon"><i class="fa fa-vcard"></i></div>
-                                        <div class="stat">
-                                            <div class="value"> {$todayuserlogin}</div>
-                                            <div class="name"> {:__('Today user login')}</div>
-                                        </div>
-                                        <div class="progress">
-                                            <div class="progress-bar progress-bar-success" style="width: 20%"></div>
-                                        </div>
-                                    </div>
-                                    <div class="col-xs-6  stat-col">
-                                        <div class="stat-icon"><i class="fa fa-calendar"></i></div>
-                                        <div class="stat">
-                                            <div class="value"> {$threednu}</div>
-                                            <div class="name"> {:__('Three dnu')}</div>
-                                        </div>
-                                        <div class="progress">
-                                            <div class="progress-bar progress-bar-success" style="width: 20%"></div>
-                                        </div>
-                                    </div>
-                                    <div class="col-xs-6 stat-col">
-                                        <div class="stat-icon"><i class="fa fa-calendar-plus-o"></i></div>
-                                        <div class="stat">
-                                            <div class="value"> {$sevendnu}</div>
-                                            <div class="name"> {:__('Seven dnu')}</div>
-                                        </div>
-                                        <div class="progress">
-                                            <div class="progress-bar progress-bar-success" style="width: 20%"></div>
-                                        </div>
-                                    </div>
-                                    <div class="col-xs-6  stat-col">
-                                        <div class="stat-icon"><i class="fa fa-user-circle"></i></div>
-                                        <div class="stat">
-                                            <div class="value"> {$sevendau}</div>
-                                            <div class="name"> {:__('Seven dau')}</div>
-                                        </div>
-                                        <div class="progress">
-                                            <div class="progress-bar progress-bar-success" style="width: 20%"></div>
-                                        </div>
-                                    </div>
-                                    <div class="col-xs-6  stat-col">
-                                        <div class="stat-icon"><i class="fa fa-user-circle-o"></i></div>
-                                        <div class="stat">
-                                            <div class="value"> {$thirtydau}</div>
-                                            <div class="name"> {:__('Thirty dau')}</div>
-                                        </div>
-                                        <div class="progress">
-                                            <div class="progress-bar progress-bar-success" style="width: 20%"></div>
-                                        </div>
-                                    </div>
+        <div class="proc-dash">
+                    <div class="proc-kpi-section">
+                        <div class="proc-kpi-section-title"><i class="fa fa-calendar-o"></i> 今日数据</div>
+                        <div class="proc-dash-overview cols-3">
+                            <div class="proc-overview-item item-confirm">
+                                <div class="overview-icon"><i class="fa fa-check-square-o"></i></div>
+                                <div class="overview-body">
+                                    <div class="overview-title">待确认</div>
+                                    <div class="overview-num overview-num-single">{$kpi.confirm.today|default='0'}</div>
+                                    <a href="javascript:;" class="overview-link addtabsit" data-url="procuremen/audit"><i class="fa fa-external-link"></i> 供应商确认</a>
                                 </div>
                             </div>
-                        </div>
-                    </div>
-                </div>
-
-                <div class="row" style="margin-top:15px;" id="statistics">
-
-                    <div class="col-lg-12">
-                    </div>
-                    <div class="col-xs-6 col-md-3">
-                        <div class="panel bg-blue-gradient no-border">
-                            <div class="panel-body">
-                                <div class="panel-title">
-                                    <span class="label label-primary pull-right">{:__('Real time')}</span>
-                                    <h5>{:__('Working addon count')}</h5>
+                            <div class="proc-overview-item item-approval">
+                                <div class="overview-icon"><i class="fa fa-shopping-cart"></i></div>
+                                <div class="overview-body">
+                                    <div class="overview-title">待审批</div>
+                                    <div class="overview-num overview-num-single">{$kpi.approval.today|default='0'}</div>
+                                    <a href="javascript:;" class="overview-link addtabsit" data-url="procuremen/confirm"><i class="fa fa-external-link"></i> 供应商审批</a>
                                 </div>
-                                <div class="panel-content">
-                                    <div class="row">
-                                        <div class="col-md-12">
-                                            <h1 class="no-margins">{$totalworkingaddon}</h1>
-                                            <div class="font-bold"><i class="fa fa-magic"></i>
-                                                <small>{:__('Working addon count tips')}</small>
-                                            </div>
-                                        </div>
-                                    </div>
+                            </div>
+                            <div class="proc-overview-item item-completed">
+                                <div class="overview-icon"><i class="fa fa-flag-checkered"></i></div>
+                                <div class="overview-body">
+                                    <div class="overview-title">已完结</div>
+                                    <div class="overview-num overview-num-single">{$kpi.completed.today|default='0'}</div>
+                                    <a href="javascript:;" class="overview-link addtabsit" data-url="procuremenarchive/index"><i class="fa fa-external-link"></i> 历史存证档案查询</a>
                                 </div>
                             </div>
                         </div>
                     </div>
-                    <div class="col-xs-6 col-md-3">
-                        <div class="panel bg-teal-gradient no-border">
-                            <div class="panel-body">
-                                <div class="ibox-title">
-                                    <span class="label label-primary pull-right">{:__('Real time')}</span>
-                                    <h5>{:__('Database count')}</h5>
-                                </div>
-                                <div class="ibox-content">
-                                    <div class="row">
-                                        <div class="col-md-6">
-                                            <h1 class="no-margins">{$dbtablenums}</h1>
-                                            <div class="font-bold"><i class="fa fa-database"></i>
-                                                <small>{:__('Database table nums')}</small>
-                                            </div>
-                                        </div>
-                                        <div class="col-md-6">
-                                            <h1 class="no-margins">{$dbsize|format_bytes=###,'',0}</h1>
-                                            <div class="font-bold"><i class="fa fa-filter"></i>
-                                                <small>{:__('Database size')}</small>
-                                            </div>
-                                        </div>
-                                    </div>
+                    <div class="proc-kpi-section">
+                        <div class="proc-kpi-section-title"><i class="fa fa-calendar"></i> 本月数据</div>
+                        <div class="proc-dash-overview cols-3">
+                            <div class="proc-overview-item item-confirm">
+                                <div class="overview-icon"><i class="fa fa-check-square-o"></i></div>
+                                <div class="overview-body">
+                                    <div class="overview-title">待确认</div>
+                                    <div class="overview-num overview-num-single">{$kpi.confirm.month|default='0'}</div>
+                                    <a href="javascript:;" class="overview-link addtabsit" data-url="procuremen/audit"><i class="fa fa-external-link"></i> 供应商确认</a>
                                 </div>
                             </div>
-                        </div>
-                    </div>
-
-                    <div class="col-xs-6 col-md-3">
-                        <div class="panel bg-purple-gradient no-border">
-                            <div class="panel-body">
-                                <div class="ibox-title">
-                                    <span class="label label-primary pull-right">{:__('Real time')}</span>
-                                    <h5>{:__('Attachment count')}</h5>
+                            <div class="proc-overview-item item-approval">
+                                <div class="overview-icon"><i class="fa fa-shopping-cart"></i></div>
+                                <div class="overview-body">
+                                    <div class="overview-title">待审批</div>
+                                    <div class="overview-num overview-num-single">{$kpi.approval.month|default='0'}</div>
+                                    <a href="javascript:;" class="overview-link addtabsit" data-url="procuremen/confirm"><i class="fa fa-external-link"></i> 供应商审批</a>
                                 </div>
-                                <div class="ibox-content">
-
-                                    <div class="row">
-                                        <div class="col-md-6">
-                                            <h1 class="no-margins">{$attachmentnums}</h1>
-                                            <div class="font-bold"><i class="fa fa-files-o"></i>
-                                                <small>{:__('Attachment nums')}</small>
-                                            </div>
-                                        </div>
-                                        <div class="col-md-6">
-                                            <h1 class="no-margins">{$attachmentsize|format_bytes=###,'',0}</h1>
-                                            <div class="font-bold"><i class="fa fa-filter"></i>
-                                                <small>{:__('Attachment size')}</small>
-                                            </div>
-                                        </div>
-                                    </div>
+                            </div>
+                            <div class="proc-overview-item item-completed">
+                                <div class="overview-icon"><i class="fa fa-flag-checkered"></i></div>
+                                <div class="overview-body">
+                                    <div class="overview-title">已完结</div>
+                                    <div class="overview-num overview-num-single">{$kpi.completed.month|default='0'}</div>
+                                    <a href="javascript:;" class="overview-link addtabsit" data-url="procuremenarchive/index"><i class="fa fa-external-link"></i> 历史存证档案查询</a>
                                 </div>
                             </div>
                         </div>
                     </div>
-                    <div class="col-xs-6 col-md-3">
-                        <div class="panel bg-green-gradient no-border">
-                            <div class="panel-body">
-                                <div class="ibox-title">
-                                    <span class="label label-primary pull-right">{:__('Real time')}</span>
-                                    <h5>{:__('Picture count')}</h5>
+                    <div class="proc-dash-chart-row">
+                        <div class="chart-col">
+                            <div id="procuremen-echart" class="btn-refresh"></div>
+                        </div>
+                        <div class="side-col">
+                            <div class="proc-summary-panel proc-rank-panel">
+                                <div class="rank-head">
+                                    <span class="rank-title"><i class="fa fa-bar-chart"></i> 供应商排名</span>
+                                    <span class="rank-supplier-inline">供应商总数 <strong>{$supplierRank.supplier_total|default='0'}</strong></span>
                                 </div>
-                                <div class="ibox-content">
-
-                                    <div class="row">
-                                        <div class="col-md-6">
-                                            <h1 class="no-margins">{$picturenums}</h1>
-                                            <div class="font-bold"><i class="fa fa-picture-o"></i>
-                                                <small>{:__('Picture nums')}</small>
+                                <ul class="nav nav-tabs rank-tabs">
+                                    <li class="active"><a href="#rank-quote" data-toggle="tab">参与报价</a></li>
+                                    <li><a href="#rank-selected" data-toggle="tab">被选中</a></li>
+                                </ul>
+                                <div class="tab-content rank-list">
+                                    <div class="tab-pane active" id="rank-quote">
+                                        {notempty name="supplierRank.quote"}
+                                        {foreach name="supplierRank.quote" item="item"}
+                                        <div class="rank-item">
+                                            <span class="rank-no rank-no-{$item.rank|default='0'}">{$item.rank|default='0'}</span>
+                                            <div class="rank-body">
+                                                <div class="rank-name-row">
+                                                    <span class="rank-name" title="{$item.name|default=''|htmlentities}">{$item.name|default=''|htmlentities}</span>
+                                                    <span class="rank-value">{$item.value|default='0'}</span>
+                                                </div>
+                                                <div class="progress rank-bar">
+                                                    <div class="progress-bar" style="width:{$item.percent|default='0'}%"></div>
+                                                </div>
                                             </div>
                                         </div>
-                                        <div class="col-md-6">
-                                            <h1 class="no-margins">{$picturesize|format_bytes=###,'',0}</h1>
-                                            <div class="font-bold"><i class="fa fa-filter"></i>
-                                                <small>{:__('Picture size')}</small>
+                                        {/foreach}
+                                        {else /}
+                                        <div class="rank-empty">本月暂无报价数据</div>
+                                        {/notempty}
+                                    </div>
+                                    <div class="tab-pane" id="rank-selected">
+                                        {notempty name="supplierRank.selected"}
+                                        {foreach name="supplierRank.selected" item="item"}
+                                        <div class="rank-item">
+                                            <span class="rank-no rank-no-{$item.rank|default='0'}">{$item.rank|default='0'}</span>
+                                            <div class="rank-body">
+                                                <div class="rank-name-row">
+                                                    <span class="rank-name" title="{$item.name|default=''|htmlentities}">{$item.name|default=''|htmlentities}</span>
+                                                    <span class="rank-value">{$item.value|default='0'}</span>
+                                                </div>
+                                                <div class="progress rank-bar">
+                                                    <div class="progress-bar" style="width:{$item.percent|default='0'}%"></div>
+                                                </div>
                                             </div>
                                         </div>
+                                        {/foreach}
+                                        {else /}
+                                        <div class="rank-empty">本月暂无选中记录</div>
+                                        {/notempty}
                                     </div>
                                 </div>
                             </div>
                         </div>
                     </div>
                 </div>
-            </div>
-            <div class="tab-pane fade" id="two">
-                <div class="row">
-                    <div class="col-xs-12">
-                        {:__('Custom zone')}
-                    </div>
-                </div>
-            </div>
         </div>
     </div>
 </div>

+ 403 - 0
application/admin/view/dashboard/驾驶舱控制台页面.html

@@ -0,0 +1,403 @@
+<style type="text/css">
+    .sm-st {
+        background: #fff;
+        padding: 20px;
+        -webkit-border-radius: 3px;
+        -moz-border-radius: 3px;
+        border-radius: 3px;
+        margin-bottom: 20px;
+    }
+
+    .sm-st-icon {
+        width: 60px;
+        height: 60px;
+        display: inline-block;
+        line-height: 60px;
+        text-align: center;
+        font-size: 30px;
+        background: #eee;
+        -webkit-border-radius: 5px;
+        -moz-border-radius: 5px;
+        border-radius: 5px;
+        float: left;
+        margin-right: 10px;
+        color: #fff;
+    }
+
+    .sm-st-info {
+        padding-top: 2px;
+    }
+
+    .sm-st-info span {
+        display: block;
+        font-size: 24px;
+        font-weight: 600;
+    }
+
+    .orange {
+        background: #fa8564 !important;
+    }
+
+    .tar {
+        background: #45cf95 !important;
+    }
+
+    .sm-st .green {
+        background: #86ba41 !important;
+    }
+
+    .pink {
+        background: #AC75F0 !important;
+    }
+
+    .yellow-b {
+        background: #fdd752 !important;
+    }
+
+    .stat-elem {
+
+        background-color: #fff;
+        padding: 18px;
+        border-radius: 40px;
+
+    }
+
+    .stat-info {
+        text-align: center;
+        background-color: #fff;
+        border-radius: 5px;
+        margin-top: -5px;
+        padding: 8px;
+        -webkit-box-shadow: 0 1px 0px rgba(0, 0, 0, 0.05);
+        box-shadow: 0 1px 0px rgba(0, 0, 0, 0.05);
+        font-style: italic;
+    }
+
+    .stat-icon {
+        text-align: center;
+        margin-bottom: 5px;
+    }
+
+    .st-red {
+        background-color: #F05050;
+    }
+
+    .st-green {
+        background-color: #27C24C;
+    }
+
+    .st-violet {
+        background-color: #7266ba;
+    }
+
+    .st-blue {
+        background-color: #23b7e5;
+    }
+
+    .stats .stat-icon {
+        color: #28bb9c;
+        display: inline-block;
+        font-size: 26px;
+        text-align: center;
+        vertical-align: middle;
+        width: 50px;
+        float: left;
+    }
+
+    .stat {
+        white-space: nowrap;
+        overflow: hidden;
+        text-overflow: ellipsis;
+        display: inline-block;
+    }
+
+    .stat .value {
+        font-size: 20px;
+        line-height: 24px;
+        overflow: hidden;
+        text-overflow: ellipsis;
+        font-weight: 500;
+    }
+
+    .stat .name {
+        overflow: hidden;
+        text-overflow: ellipsis;
+        margin: 5px 0;
+    }
+
+    .stat.lg .value {
+        font-size: 26px;
+        line-height: 28px;
+    }
+
+    .stat-col {
+        margin:0 0 10px 0;
+    }
+    .stat.lg .name {
+        font-size: 16px;
+    }
+
+    .stat-col .progress {
+        height: 2px;
+    }
+
+    .stat-col .progress-bar {
+        line-height: 2px;
+        height: 2px;
+    }
+
+    .item {
+        padding: 30px 0;
+    }
+
+
+    #statistics .panel {
+        min-height: 150px;
+    }
+
+    #statistics .panel h5 {
+        font-size: 14px;
+    }
+</style>
+<div class="panel panel-default panel-intro">
+    <div class="panel-heading">
+        {:build_heading(null, false)}
+        <ul class="nav nav-tabs">
+            <li class="active"><a href="#one" data-toggle="tab">{:__('Dashboard')}</a></li>
+            <li><a href="#two" data-toggle="tab">{:__('Custom')}</a></li>
+        </ul>
+    </div>
+    <div class="panel-body">
+        <div id="myTabContent" class="tab-content">
+            <div class="tab-pane fade active in" id="one">
+
+                <div class="row">
+                    <div class="col-sm-3 col-xs-6">
+                        <div class="sm-st clearfix">
+                            <span class="sm-st-icon st-red"><i class="fa fa-users"></i></span>
+                            <div class="sm-st-info">
+                                <span>{$totaluser}</span>
+                                {:__('Total user')}
+                            </div>
+                        </div>
+                    </div>
+                    <div class="col-sm-3 col-xs-6">
+                        <div class="sm-st clearfix">
+                            <span class="sm-st-icon st-violet"><i class="fa fa-magic"></i></span>
+                            <div class="sm-st-info">
+                                <span>{$totaladdon}</span>
+                                {:__('Total addon')}
+                            </div>
+                        </div>
+                    </div>
+                    <div class="col-sm-3 col-xs-6">
+                        <div class="sm-st clearfix">
+                            <span class="sm-st-icon st-blue"><i class="fa fa-leaf"></i></span>
+                            <div class="sm-st-info">
+                                <span>{$attachmentnums}</span>
+                                {:__('Total attachment')}
+                            </div>
+                        </div>
+                    </div>
+                    <div class="col-sm-3 col-xs-6">
+                        <div class="sm-st clearfix">
+                            <span class="sm-st-icon st-green"><i class="fa fa-user"></i></span>
+                            <div class="sm-st-info">
+                                <span>{$totaladmin}</span>
+                                {:__('Total admin')}
+                            </div>
+                        </div>
+                    </div>
+                </div>
+
+                <div class="row">
+                    <div class="col-lg-8">
+                        <div id="echart" class="btn-refresh" style="height:300px;width:100%;"></div>
+                    </div>
+                    <div class="col-lg-4">
+                        <div class="card sameheight-item stats">
+                            <div class="card-block">
+                                <div class="row row-sm stats-container">
+                                    <div class="col-xs-6 stat-col">
+                                        <div class="stat-icon"><i class="fa fa-rocket"></i></div>
+                                        <div class="stat">
+                                            <div class="value"> {$todayusersignup}</div>
+                                            <div class="name"> {:__('Today user signup')}</div>
+                                        </div>
+                                        <div class="progress">
+                                            <div class="progress-bar progress-bar-success" style="width: 20%"></div>
+                                        </div>
+                                    </div>
+                                    <div class="col-xs-6 stat-col">
+                                        <div class="stat-icon"><i class="fa fa-vcard"></i></div>
+                                        <div class="stat">
+                                            <div class="value"> {$todayuserlogin}</div>
+                                            <div class="name"> {:__('Today user login')}</div>
+                                        </div>
+                                        <div class="progress">
+                                            <div class="progress-bar progress-bar-success" style="width: 20%"></div>
+                                        </div>
+                                    </div>
+                                    <div class="col-xs-6  stat-col">
+                                        <div class="stat-icon"><i class="fa fa-calendar"></i></div>
+                                        <div class="stat">
+                                            <div class="value"> {$threednu}</div>
+                                            <div class="name"> {:__('Three dnu')}</div>
+                                        </div>
+                                        <div class="progress">
+                                            <div class="progress-bar progress-bar-success" style="width: 20%"></div>
+                                        </div>
+                                    </div>
+                                    <div class="col-xs-6 stat-col">
+                                        <div class="stat-icon"><i class="fa fa-calendar-plus-o"></i></div>
+                                        <div class="stat">
+                                            <div class="value"> {$sevendnu}</div>
+                                            <div class="name"> {:__('Seven dnu')}</div>
+                                        </div>
+                                        <div class="progress">
+                                            <div class="progress-bar progress-bar-success" style="width: 20%"></div>
+                                        </div>
+                                    </div>
+                                    <div class="col-xs-6  stat-col">
+                                        <div class="stat-icon"><i class="fa fa-user-circle"></i></div>
+                                        <div class="stat">
+                                            <div class="value"> {$sevendau}</div>
+                                            <div class="name"> {:__('Seven dau')}</div>
+                                        </div>
+                                        <div class="progress">
+                                            <div class="progress-bar progress-bar-success" style="width: 20%"></div>
+                                        </div>
+                                    </div>
+                                    <div class="col-xs-6  stat-col">
+                                        <div class="stat-icon"><i class="fa fa-user-circle-o"></i></div>
+                                        <div class="stat">
+                                            <div class="value"> {$thirtydau}</div>
+                                            <div class="name"> {:__('Thirty dau')}</div>
+                                        </div>
+                                        <div class="progress">
+                                            <div class="progress-bar progress-bar-success" style="width: 20%"></div>
+                                        </div>
+                                    </div>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+
+                <div class="row" style="margin-top:15px;" id="statistics">
+
+                    <div class="col-lg-12">
+                    </div>
+                    <div class="col-xs-6 col-md-3">
+                        <div class="panel bg-blue-gradient no-border">
+                            <div class="panel-body">
+                                <div class="panel-title">
+                                    <span class="label label-primary pull-right">{:__('Real time')}</span>
+                                    <h5>{:__('Working addon count')}</h5>
+                                </div>
+                                <div class="panel-content">
+                                    <div class="row">
+                                        <div class="col-md-12">
+                                            <h1 class="no-margins">{$totalworkingaddon}</h1>
+                                            <div class="font-bold"><i class="fa fa-magic"></i>
+                                                <small>{:__('Working addon count tips')}</small>
+                                            </div>
+                                        </div>
+                                    </div>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                    <div class="col-xs-6 col-md-3">
+                        <div class="panel bg-teal-gradient no-border">
+                            <div class="panel-body">
+                                <div class="ibox-title">
+                                    <span class="label label-primary pull-right">{:__('Real time')}</span>
+                                    <h5>{:__('Database count')}</h5>
+                                </div>
+                                <div class="ibox-content">
+                                    <div class="row">
+                                        <div class="col-md-6">
+                                            <h1 class="no-margins">{$dbtablenums}</h1>
+                                            <div class="font-bold"><i class="fa fa-database"></i>
+                                                <small>{:__('Database table nums')}</small>
+                                            </div>
+                                        </div>
+                                        <div class="col-md-6">
+                                            <h1 class="no-margins">{$dbsize|format_bytes=###,'',0}</h1>
+                                            <div class="font-bold"><i class="fa fa-filter"></i>
+                                                <small>{:__('Database size')}</small>
+                                            </div>
+                                        </div>
+                                    </div>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+
+                    <div class="col-xs-6 col-md-3">
+                        <div class="panel bg-purple-gradient no-border">
+                            <div class="panel-body">
+                                <div class="ibox-title">
+                                    <span class="label label-primary pull-right">{:__('Real time')}</span>
+                                    <h5>{:__('Attachment count')}</h5>
+                                </div>
+                                <div class="ibox-content">
+
+                                    <div class="row">
+                                        <div class="col-md-6">
+                                            <h1 class="no-margins">{$attachmentnums}</h1>
+                                            <div class="font-bold"><i class="fa fa-files-o"></i>
+                                                <small>{:__('Attachment nums')}</small>
+                                            </div>
+                                        </div>
+                                        <div class="col-md-6">
+                                            <h1 class="no-margins">{$attachmentsize|format_bytes=###,'',0}</h1>
+                                            <div class="font-bold"><i class="fa fa-filter"></i>
+                                                <small>{:__('Attachment size')}</small>
+                                            </div>
+                                        </div>
+                                    </div>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                    <div class="col-xs-6 col-md-3">
+                        <div class="panel bg-green-gradient no-border">
+                            <div class="panel-body">
+                                <div class="ibox-title">
+                                    <span class="label label-primary pull-right">{:__('Real time')}</span>
+                                    <h5>{:__('Picture count')}</h5>
+                                </div>
+                                <div class="ibox-content">
+
+                                    <div class="row">
+                                        <div class="col-md-6">
+                                            <h1 class="no-margins">{$picturenums}</h1>
+                                            <div class="font-bold"><i class="fa fa-picture-o"></i>
+                                                <small>{:__('Picture nums')}</small>
+                                            </div>
+                                        </div>
+                                        <div class="col-md-6">
+                                            <h1 class="no-margins">{$picturesize|format_bytes=###,'',0}</h1>
+                                            <div class="font-bold"><i class="fa fa-filter"></i>
+                                                <small>{:__('Picture size')}</small>
+                                            </div>
+                                        </div>
+                                    </div>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+            <div class="tab-pane fade" id="two">
+                <div class="row">
+                    <div class="col-xs-12">
+                        {:__('Custom zone')}
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+</div>

+ 37 - 14
application/admin/view/procuremen/index.html

@@ -294,25 +294,48 @@
     #procuremen-layout .procuremen-op-btns .btn + .btn {
         margin-left: 6px;
     }
-    /* 可排序列表头:排序图标贴近文字,避免贴列最右侧 */
-    #procuremen-layout .bootstrap-table thead th.procuremen-th-sort-tight {
-        background-position: calc(100% - 8px) center !important;
-    }
-    #procuremen-layout .bootstrap-table thead th.procuremen-th-sort-tight .th-inner {
-        display: inline-block;
-        max-width: calc(100% - 4px);
-        padding-right: 16px !important;
+    /* 可排序列表头:排序图标紧跟文字后面(th-inner 仅包裹文字宽度) */
+    #procuremen-layout .bootstrap-table thead th.sortable,
+    #procuremen-layout .bootstrap-table thead th .th-inner.sortable {
+        background-image: none !important;
+        background-position: unset !important;
+        padding-right: 6px !important;
+    }
+    #procuremen-layout .bootstrap-table thead th.sortable .th-inner,
+    #procuremen-layout .bootstrap-table thead th .th-inner.sortable {
+        display: inline-block !important;
+        width: auto !important;
+        max-width: calc(100% - 2px);
+        padding-right: 0 !important;
         box-sizing: border-box;
+        vertical-align: middle;
+        white-space: nowrap;
+    }
+    #procuremen-layout .bootstrap-table thead th.sortable .th-inner:after,
+    #procuremen-layout .bootstrap-table thead th .th-inner.sortable:after {
+        content: '\f0dc';
+        font-family: FontAwesome;
+        font-size: 11px;
+        line-height: 1;
+        color: #bbb;
+        margin-left: 4px;
+        vertical-align: middle;
+    }
+    #procuremen-layout .bootstrap-table thead th.sortable.asc .th-inner:after,
+    #procuremen-layout .bootstrap-table thead th.sortable.desc .th-inner:after,
+    #procuremen-layout .bootstrap-table thead th .th-inner.sortable.asc:after,
+    #procuremen-layout .bootstrap-table thead th .th-inner.sortable.desc:after {
+        color: #337ab7;
     }
-    /* 完结确认弹层:与操作列「完结」同为警示色主按钮 */
+    /* 完结确认弹层:与「已完结」卡片、完结按钮同为绿色 */
     body .layui-layer-procuremen-finish .layui-layer-btn0 {
-        background-color: #f0ad4e !important;
-        border-color: #eea236 !important;
+        background-color: #27C24C !important;
+        border-color: #23ad44 !important;
         color: #fff !important;
     }
     body .layui-layer-procuremen-finish .layui-layer-btn0:hover {
-        background-color: #ec971f !important;
-        border-color: #d58512 !important;
+        background-color: #1e983e !important;
+        border-color: #1a8737 !important;
         color: #fff !important;
     }
     #procuremen-layout .procuremen-po-field.procuremen-po-dirty {
@@ -393,7 +416,7 @@
                                     <a href="javascript:;" class="btn btn-info" id="btn-procuremen-pick-review" title="勾选一条或多条工序进行下发(多条须同一订单号)" style="display:none;"><i class="fa fa-paper-plane"></i> 下发</a>
                                     {/if}
                                     {if isset($procuremenBtnComplete) && $procuremenBtnComplete}
-                                    <a href="javascript:;" class="btn btn-warning" id="btn-procuremen-batch-finish" title="勾选一条或多条工序标记为已完结" style="display:none;"><i class="fa fa-flag-checkered"></i> 完结</a>
+                                    <a href="javascript:;" class="btn btn-success" id="btn-procuremen-batch-finish" title="勾选一条或多条工序标记为已完结" style="display:none;"><i class="fa fa-flag-checkered"></i> 完结</a>
                                     {/if}
                                     {if isset($procuremenBtnPickDelete) && $procuremenBtnPickDelete}
                                     <a href="javascript:;" class="btn btn-danger" id="btn-procuremen-pick-delete" title="勾选一条或多条工序从初选列表移除" style="display:none;"><i class="fa fa-trash"></i> 删除</a>

+ 885 - 0
application/common/library/ProcuremenDashboard.php

@@ -0,0 +1,885 @@
+<?php
+
+namespace app\common\library;
+
+use think\Db;
+
+/**
+ * 控制台 — 协助采购(采购供应商)统计
+ */
+class ProcuremenDashboard
+{
+    /**
+     * @return array{
+     *   today: array{issued:int,confirm:int,approval:int,completed:int},
+     *   week: array{issued:int,confirm:int,approval:int,completed:int},
+     *   pending: array{confirm:int,approval:int},
+     *   supplierRank: array{quote: array, selected: array, period_label: string},
+     *   kpi: array<string, array{today:int,month:int,link:string,link_text:string}>,
+     *   updated: string
+     * }
+     */
+    public static function buildStats(): array
+    {
+        $todayStart = date('Y-m-d 00:00:00');
+        $todayEnd = date('Y-m-d H:i:s');
+        $weekStart = date('Y-m-d 00:00:00', strtotime('monday this week'));
+        $weekEnd = date('Y-m-d H:i:s');
+
+        return [
+            'kpi'          => self::buildKpiCards(),
+            'today'        => self::countStageMetrics($todayStart, $todayEnd),
+            'week'         => self::countStageMetrics($weekStart, $weekEnd),
+            'pending'      => self::countCurrentPending(),
+            'supplierRank' => self::buildSupplierRankings(10, date('Y-m-01 00:00:00'), date('Y-m-t 23:59:59')),
+            'last7'        => self::buildLast7DaysChart(),
+            'updated'      => date('Y-m-d H:i:s'),
+        ];
+    }
+
+    /**
+     * 控制台首行 KPI:今日 / 本月
+     *
+     * @return array<string, array{today:int,month:int,link:string,link_text:string}>
+     */
+    public static function buildKpiCards(): array
+    {
+        $todayStart = date('Y-m-d 00:00:00');
+        $todayEnd = date('Y-m-d H:i:s');
+        $monthStart = date('Y-m-01 00:00:00');
+        $monthEnd = date('Y-m-t 23:59:59');
+        $pending = self::countCurrentPending();
+
+        return [
+            'supplier' => [
+                'total'     => self::countActiveSuppliers(),
+                'link'      => '',
+                'link_text' => '',
+            ],
+            'confirm' => [
+                'today'     => (int)$pending['confirm'],
+                'month'     => self::countOperLogDistinctOrdersInRange(['issue_submit'], $monthStart, $monthEnd),
+                'link'      => 'procuremen/audit',
+                'link_text' => '供应商确认',
+            ],
+            'approval' => [
+                'today'     => (int)$pending['approval'],
+                'month'     => self::countOperLogDistinctOrdersInRange(['audit_select'], $monthStart, $monthEnd),
+                'link'      => 'procuremen/confirm',
+                'link_text' => '供应商审批',
+            ],
+            'completed' => [
+                'today'     => self::countCompletedInRange($todayStart, $todayEnd),
+                'month'     => self::countCompletedInRange($monthStart, $monthEnd),
+                'link'      => 'procuremenarchive/index',
+                'link_text' => '历史存证档案查询',
+            ],
+        ];
+    }
+
+    /**
+     * @param string[] $wflowValues
+     */
+    protected static function countWflowOrdersInPickRange(array $wflowValues, string $start, string $end): int
+    {
+        try {
+            $expr = self::pickTimeSqlExpr();
+            $query = Db::table('purchase_order')->field('id,CCYDH');
+            self::applyNotDeletedWhere($query);
+            $query->whereIn('wflow_status', $wflowValues);
+            $query->whereRaw("({$expr}) >= ? AND ({$expr}) <= ?", [$start, $end]);
+            $rows = $query->select();
+        } catch (\Throwable $e) {
+            return 0;
+        }
+
+        return self::countDistinctOrders(is_array($rows) ? $rows : []);
+    }
+
+    protected static function countQuoteSuppliersInPickRange(string $start, string $end): int
+    {
+        $scydgyIds = self::loadScydgyIdsInPickTimeRange($start, $end);
+        if ($scydgyIds === []) {
+            return 0;
+        }
+        try {
+            $rows = Db::table('purchase_order_detail')
+                ->where('scydgy_id', 'in', $scydgyIds)
+                ->field('company_name,amount')
+                ->select();
+        } catch (\Throwable $e) {
+            return 0;
+        }
+        if (!is_array($rows)) {
+            return 0;
+        }
+        $names = [];
+        foreach ($rows as $row) {
+            if (!is_array($row)) {
+                continue;
+            }
+            $name = trim((string)($row['company_name'] ?? ''));
+            $amount = trim((string)($row['amount'] ?? ''));
+            if ($name === '' || $amount === '' || $amount === '0' || $amount === '0.00') {
+                continue;
+            }
+            $names[$name] = true;
+        }
+
+        return count($names);
+    }
+
+    protected static function countActiveSuppliers(): int
+    {
+        try {
+            $query = Db::table('customer');
+            $query->where(function ($q) {
+                $q->where('status', 1)
+                    ->whereOr('status', '1')
+                    ->whereOr('status', '')
+                    ->whereNull('status');
+            });
+
+            return (int)$query->count();
+        } catch (\Throwable $e) {
+            return self::countDistinctSuppliersFromDetails();
+        }
+    }
+
+    protected static function countDistinctSuppliersFromDetails(): int
+    {
+        try {
+            $rows = Db::table('purchase_order_detail')
+                ->field('company_name')
+                ->select();
+        } catch (\Throwable $e) {
+            return 0;
+        }
+        if (!is_array($rows)) {
+            return 0;
+        }
+        $names = [];
+        foreach ($rows as $row) {
+            if (!is_array($row)) {
+                continue;
+            }
+            $name = trim((string)($row['company_name'] ?? ''));
+            if ($name !== '') {
+                $names[$name] = true;
+            }
+        }
+
+        return count($names);
+    }
+
+    protected static function countDistinctOrdersInPickTimeRange(string $start, string $end): int
+    {
+        try {
+            $expr = self::pickTimeSqlExpr();
+            $query = Db::table('purchase_order')->field('id,CCYDH');
+            self::applyNotDeletedWhere($query);
+            $query->whereRaw("({$expr}) >= ? AND ({$expr}) <= ?", [$start, $end]);
+            $rows = $query->select();
+        } catch (\Throwable $e) {
+            return 0;
+        }
+
+        return self::countDistinctOrders(is_array($rows) ? $rows : []);
+    }
+
+    /**
+     * 供应商排名(本月):参与报价次数 / 被选中次数
+     *
+     * @return array{
+     *   quote: array<int, array{rank:int,name:string,value:int,percent:int}>,
+     *   selected: array<int, array{rank:int,name:string,value:int,percent:int}>,
+     *   period_label: string,
+     *   supplier_total: int
+     * }
+     */
+    public static function buildSupplierRankings(int $limit = 10, ?string $start = null, ?string $end = null): array
+    {
+        $start = $start ?: date('Y-m-d 00:00:00', strtotime('first day of this month'));
+        $end = $end ?: date('Y-m-t 23:59:59');
+
+        return [
+            'quote'          => self::rankSuppliersByQuoteInRange($start, $end, $limit),
+            'selected'       => self::rankSuppliersBySelectedInRange($start, $end, $limit),
+            'period_label'   => '本月',
+            'supplier_total' => self::countActiveSuppliers(),
+        ];
+    }
+
+    /**
+     * @param array<string, int> $counts
+     * @return array<int, array{rank:int,name:string,value:int,percent:int}>
+     */
+    protected static function formatSupplierRankListFromCounts(array $counts, int $limit): array
+    {
+        if ($counts === []) {
+            return [];
+        }
+        arsort($counts);
+        $top = array_slice($counts, 0, max(1, $limit), true);
+        $max = $top ? max($top) : 0;
+        $out = [];
+        $rank = 1;
+        foreach ($top as $name => $value) {
+            $out[] = [
+                'rank'    => $rank++,
+                'name'    => (string)$name,
+                'value'   => (int)$value,
+                'percent' => $max > 0 ? (int)round($value / $max * 100) : 0,
+            ];
+        }
+
+        return $out;
+    }
+
+    /**
+     * @param array<string, array<string|int, bool>> $buckets
+     * @return array<int, array{rank:int,name:string,value:int,percent:int}>
+     */
+    protected static function formatSupplierRankList(array $buckets, int $limit): array
+    {
+        if ($buckets === []) {
+            return [];
+        }
+        $counts = [];
+        foreach ($buckets as $name => $keys) {
+            $counts[$name] = is_array($keys) ? count($keys) : 0;
+        }
+        arsort($counts);
+        $top = array_slice($counts, 0, max(1, $limit), true);
+        $max = $top ? max($top) : 0;
+        $out = [];
+        $rank = 1;
+        foreach ($top as $name => $value) {
+            $out[] = [
+                'rank'    => $rank++,
+                'name'    => (string)$name,
+                'value'   => (int)$value,
+                'percent' => $max > 0 ? (int)round($value / $max * 100) : 0,
+            ];
+        }
+
+        return $out;
+    }
+
+    /**
+     * 按已填写报价金额的供应商统计参与报价次数(明细行数)
+     *
+     * @return array<int, array{rank:int,name:string,value:int,percent:int}>
+     */
+    protected static function rankSuppliersByQuoteInRange(string $start, string $end, int $limit): array
+    {
+        $scydgyIds = self::loadScydgyIdsInPickTimeRange($start, $end);
+        if ($scydgyIds === []) {
+            return [];
+        }
+        try {
+            $rows = Db::table('purchase_order_detail')
+                ->where('scydgy_id', 'in', $scydgyIds)
+                ->field('company_name,amount')
+                ->select();
+        } catch (\Throwable $e) {
+            return [];
+        }
+        if (!is_array($rows)) {
+            return [];
+        }
+        $counts = [];
+        foreach ($rows as $row) {
+            if (!is_array($row)) {
+                continue;
+            }
+            $name = trim((string)($row['company_name'] ?? ''));
+            $amount = trim((string)($row['amount'] ?? ''));
+            if ($name === '' || $amount === '' || $amount === '0' || $amount === '0.00') {
+                continue;
+            }
+            $counts[$name] = ($counts[$name] ?? 0) + 1;
+        }
+
+        return self::formatSupplierRankListFromCounts($counts, $limit);
+    }
+
+    /**
+     * 按主表 pick_company_name 统计被选中订单数
+     *
+     * @return array<int, array{rank:int,name:string,value:int,percent:int}>
+     */
+    protected static function rankSuppliersBySelectedInRange(string $start, string $end, int $limit): array
+    {
+        try {
+            $expr = self::pickTimeSqlExpr();
+            $query = Db::table('purchase_order')
+                ->field('CCYDH,pick_company_name');
+            self::applyNotDeletedWhere($query);
+            $query->whereRaw("({$expr}) >= ? AND ({$expr}) <= ?", [$start, $end]);
+            $query->whereRaw("pick_company_name IS NOT NULL AND TRIM(CAST(pick_company_name AS CHAR(255))) <> ''");
+            $rows = $query->select();
+        } catch (\Throwable $e) {
+            return [];
+        }
+        if (!is_array($rows)) {
+            return [];
+        }
+        $buckets = [];
+        foreach ($rows as $row) {
+            if (!is_array($row)) {
+                continue;
+            }
+            $name = trim((string)($row['pick_company_name'] ?? ''));
+            if ($name === '') {
+                continue;
+            }
+            $orderKey = self::resolveOrderKey($row);
+            if (!isset($buckets[$name])) {
+                $buckets[$name] = [];
+            }
+            $buckets[$name][$orderKey] = true;
+        }
+
+        return self::formatSupplierRankList($buckets, $limit);
+    }
+
+    /**
+     * @return int[]
+     */
+    protected static function loadScydgyIdsInPickTimeRange(string $start, string $end): array
+    {
+        try {
+            $expr = self::pickTimeSqlExpr();
+            $query = Db::table('purchase_order')->field('scydgy_id');
+            self::applyNotDeletedWhere($query);
+            $query->whereRaw("({$expr}) >= ? AND ({$expr}) <= ?", [$start, $end]);
+            $rows = $query->select();
+        } catch (\Throwable $e) {
+            return [];
+        }
+        if (!is_array($rows)) {
+            return [];
+        }
+        $ids = [];
+        foreach ($rows as $row) {
+            if (!is_array($row)) {
+                continue;
+            }
+            $sid = (int)($row['scydgy_id'] ?? 0);
+            if ($sid > 0) {
+                $ids[$sid] = true;
+            }
+        }
+
+        return array_keys($ids);
+    }
+
+    /**
+     * 近 7 天每日趋势(按订单号去重)
+     *
+     * @return array{
+     *   labels: string[],
+     *   issued: int[],
+     *   confirm: int[],
+     *   approval: int[],
+     *   completed: int[]
+     * }
+     */
+    public static function buildLast7DaysChart(): array
+    {
+        $dayKeys = [];
+        $labels = [];
+        for ($i = 6; $i >= 0; $i--) {
+            $day = date('Y-m-d', strtotime('-' . $i . ' days'));
+            $dayKeys[] = $day;
+            $labels[] = date('m-d', strtotime($day));
+        }
+        if ($dayKeys === []) {
+            return ['labels' => [], 'issued' => [], 'confirm' => [], 'approval' => [], 'completed' => []];
+        }
+
+        $rangeStart = $dayKeys[0] . ' 00:00:00';
+        $rangeEnd = $dayKeys[count($dayKeys) - 1] . ' 23:59:59';
+        $startTs = ProcuremenTime::parseToTimestamp($rangeStart);
+        $endTs = ProcuremenTime::parseToTimestamp($rangeEnd);
+
+        $issuedBuckets = self::bucketOperLogOrdersByDay(
+            self::loadOperLogsInRange(['issue_submit'], $startTs, $endTs),
+            $dayKeys,
+            $startTs,
+            $endTs
+        );
+        $confirmBuckets = self::emptyDayBuckets($dayKeys);
+
+        try {
+            $expr = self::pickTimeSqlExpr();
+            $query = Db::table('purchase_order')->field('CCYDH,pick_time,createtime,dputrecord,dStamp,wflow_status,status');
+            self::applyNotDeletedWhere($query);
+            $query->whereRaw("({$expr}) >= ? AND ({$expr}) <= ?", [$rangeStart, $rangeEnd]);
+            $rows = $query->select();
+        } catch (\Throwable $e) {
+            $rows = [];
+        }
+        if (is_array($rows)) {
+            foreach ($rows as $row) {
+                if (!is_array($row)) {
+                    continue;
+                }
+                $day = self::resolveRowDayKey($row, $dayKeys);
+                if ($day === null) {
+                    continue;
+                }
+                $orderKey = self::resolveOrderKey($row);
+                if (self::isPendingConfirmRow($row)) {
+                    $confirmBuckets[$day][$orderKey] = true;
+                }
+            }
+        }
+
+        $approvalBuckets = self::bucketOperLogOrdersByDay(
+            self::loadOperLogsInRange(['audit_select'], $startTs, $endTs),
+            $dayKeys,
+            $startTs,
+            $endTs
+        );
+        $completedBuckets = self::bucketOperLogOrdersByDay(
+            self::loadOperLogsInRange(['purchase_confirm', 'mark_complete'], $startTs, $endTs),
+            $dayKeys,
+            $startTs,
+            $endTs
+        );
+
+        return [
+            'labels'    => $labels,
+            'issued'    => self::bucketCounts($issuedBuckets, $dayKeys),
+            'confirm'   => self::bucketCounts($confirmBuckets, $dayKeys),
+            'approval'  => self::bucketCounts($approvalBuckets, $dayKeys),
+            'completed' => self::bucketCounts($completedBuckets, $dayKeys),
+        ];
+    }
+
+    /**
+     * @param string[] $dayKeys
+     * @return array<string, array<string, bool>>
+     */
+    protected static function emptyDayBuckets(array $dayKeys): array
+    {
+        $out = [];
+        foreach ($dayKeys as $day) {
+            $out[$day] = [];
+        }
+
+        return $out;
+    }
+
+    /**
+     * @param array<string, array<string, bool>> $buckets
+     * @param string[]                         $dayKeys
+     * @return int[]
+     */
+    protected static function bucketCounts(array $buckets, array $dayKeys): array
+    {
+        $out = [];
+        foreach ($dayKeys as $day) {
+            $out[] = isset($buckets[$day]) ? count($buckets[$day]) : 0;
+        }
+
+        return $out;
+    }
+
+    /**
+     * @param array<string, mixed> $row
+     * @param string[]             $dayKeys
+     */
+    protected static function resolveRowDayKey(array $row, array $dayKeys): ?string
+    {
+        $ts = 0;
+        foreach (['pick_time', 'createtime', 'dputrecord', 'dStamp'] as $key) {
+            $t = ProcuremenTime::parseToTimestamp($row[$key] ?? null);
+            if ($t > $ts) {
+                $ts = $t;
+            }
+        }
+        if ($ts <= 0) {
+            return null;
+        }
+        $day = date('Y-m-d', $ts);
+
+        return in_array($day, $dayKeys, true) ? $day : null;
+    }
+
+    /**
+     * @param array<string, mixed> $row
+     */
+    protected static function resolveOrderKey(array $row): string
+    {
+        $dh = trim((string)($row['CCYDH'] ?? ''));
+
+        return $dh !== '' ? $dh : ('_id_' . (int)($row['id'] ?? 0));
+    }
+
+    /**
+     * @param array<string, mixed> $row
+     */
+    protected static function isPendingConfirmRow(array $row): bool
+    {
+        return ProcuremenStatus::isWflowPendingConfirm($row['wflow_status'] ?? '');
+    }
+
+    /**
+     * @param array<int, string> $actions
+     * @return array<int, array{scydgy_id:int, createtime:mixed}>
+     */
+    protected static function loadOperLogsInRange(array $actions, int $startTs, int $endTs): array
+    {
+        try {
+            $logs = Db::table('purchase_order_oper_log')
+                ->where('action', 'in', $actions)
+                ->field('scydgy_id,createtime')
+                ->select();
+        } catch (\Throwable $e) {
+            return [];
+        }
+        if (!is_array($logs)) {
+            return [];
+        }
+        $out = [];
+        foreach ($logs as $log) {
+            if (!is_array($log)) {
+                continue;
+            }
+            $ts = ProcuremenTime::parseToTimestamp($log['createtime'] ?? null);
+            if ($ts < $startTs || $ts > $endTs) {
+                continue;
+            }
+            $sid = (int)($log['scydgy_id'] ?? 0);
+            if ($sid > 0) {
+                $out[] = ['scydgy_id' => $sid, 'createtime' => $log['createtime'] ?? null];
+            }
+        }
+
+        return $out;
+    }
+
+    /**
+     * @param array<int, array{scydgy_id:int, createtime:mixed}> $logs
+     * @param string[]                                           $dayKeys
+     * @return array<string, array<string, bool>>
+     */
+    protected static function bucketOperLogOrdersByDay(array $logs, array $dayKeys, int $startTs, int $endTs): array
+    {
+        $buckets = self::emptyDayBuckets($dayKeys);
+        if ($logs === []) {
+            return $buckets;
+        }
+        $sidSet = [];
+        foreach ($logs as $log) {
+            $sidSet[(int)$log['scydgy_id']] = true;
+        }
+        $orderMap = [];
+        foreach (self::loadOrdersByScydgyIds(array_keys($sidSet)) as $row) {
+            if (!is_array($row)) {
+                continue;
+            }
+            $sid = (int)($row['scydgy_id'] ?? 0);
+            if ($sid > 0) {
+                $orderMap[$sid] = self::resolveOrderKey($row);
+            }
+        }
+        foreach ($logs as $log) {
+            $sid = (int)$log['scydgy_id'];
+            if ($sid <= 0 || !isset($orderMap[$sid])) {
+                continue;
+            }
+            $ts = ProcuremenTime::parseToTimestamp($log['createtime'] ?? null);
+            if ($ts < $startTs || $ts > $endTs) {
+                continue;
+            }
+            $day = date('Y-m-d', $ts);
+            if (!isset($buckets[$day])) {
+                continue;
+            }
+            $buckets[$day][$orderMap[$sid]] = true;
+        }
+
+        return $buckets;
+    }
+
+    /**
+     * 按操作日志统计时段内去重订单数(与近7天图口径一致)
+     *
+     * @param string[] $actions
+     */
+    protected static function countOperLogDistinctOrdersInRange(array $actions, string $start, string $end): int
+    {
+        $startTs = ProcuremenTime::parseToTimestamp($start);
+        $endTs = ProcuremenTime::parseToTimestamp($end);
+        if ($startTs <= 0 || $endTs <= 0 || $actions === []) {
+            return 0;
+        }
+        $logs = self::loadOperLogsInRange($actions, $startTs, $endTs);
+        if ($logs === []) {
+            return 0;
+        }
+        $sidSet = [];
+        foreach ($logs as $log) {
+            $sid = (int)($log['scydgy_id'] ?? 0);
+            if ($sid > 0) {
+                $sidSet[$sid] = true;
+            }
+        }
+        if ($sidSet === []) {
+            return 0;
+        }
+        $keys = [];
+        foreach (self::loadOrdersByScydgyIds(array_keys($sidSet)) as $row) {
+            if (!is_array($row)) {
+                continue;
+            }
+            $keys[self::resolveOrderKey($row)] = true;
+        }
+
+        return count($keys);
+    }
+
+    /**
+     * @return array{issued:int,confirm:int,approval:int,completed:int}
+     */
+    protected static function countStageMetrics(string $start, string $end): array
+    {
+        $pending = self::countCurrentPending();
+
+        return [
+            // 已下发:本时段内通知下发,且当前已离开「待确认」队列(不与待确认重复)
+            'issued'    => self::countIssuedInRange($start, $end),
+            // 待确认 / 待审批:与列表一致,统计当前积压(不按 pick_time 截断)
+            'confirm'   => $pending['confirm'],
+            'approval'  => $pending['approval'],
+            'completed' => self::countCompletedInRange($start, $end),
+        ];
+    }
+
+    /**
+     * @return array{confirm:int,approval:int}
+     */
+    protected static function countCurrentPending(): array
+    {
+        return [
+            'confirm'  => self::countDistinctOrders(self::loadPendingConfirmRows()),
+            'approval' => self::countDistinctOrders(self::loadPendingApprovalRows()),
+        ];
+    }
+
+    protected static function countIssuedInRange(string $start, string $end): int
+    {
+        $startTs = ProcuremenTime::parseToTimestamp($start);
+        $endTs = ProcuremenTime::parseToTimestamp($end);
+        if ($startTs <= 0 || $endTs <= 0) {
+            return 0;
+        }
+
+        $confirmSidSet = self::loadPendingConfirmScydgySet();
+        $keys = [];
+
+        $logs = self::loadOperLogsInRange(['issue_submit'], $startTs, $endTs);
+        foreach ($logs as $log) {
+            $sid = (int)($log['scydgy_id'] ?? 0);
+            if ($sid <= 0 || isset($confirmSidSet[$sid])) {
+                continue;
+            }
+            foreach (self::loadOrdersByScydgyIds([$sid]) as $row) {
+                if (!is_array($row)) {
+                    continue;
+                }
+                $keys[self::resolveOrderKey($row)] = true;
+            }
+        }
+        if ($keys !== []) {
+            return count($keys);
+        }
+
+        return self::countIssuedByPickTimePastConfirm($start, $end);
+    }
+
+    /**
+     * 无下发日志时:本时段 pick_time 内且流程已进入待审批/已审核(不含仍停在待确认)
+     */
+    protected static function countIssuedByPickTimePastConfirm(string $start, string $end): int
+    {
+        try {
+            $expr = self::pickTimeSqlExpr();
+            $query = Db::table('purchase_order')->field('id,CCYDH,wflow_status');
+            self::applyNotDeletedWhere($query);
+            $query->whereRaw("({$expr}) >= ? AND ({$expr}) <= ?", [$start, $end]);
+            $query->whereIn('wflow_status', array_merge(
+                ProcuremenStatus::wflowPendingApprovalValues(),
+                ProcuremenStatus::wflowApprovedValues()
+            ));
+            $rows = $query->select();
+        } catch (\Throwable $e) {
+            return 0;
+        }
+
+        return self::countDistinctOrders(is_array($rows) ? $rows : []);
+    }
+
+    /**
+     * @return array<int, bool>
+     */
+    protected static function loadPendingConfirmScydgySet(): array
+    {
+        $set = [];
+        foreach (self::loadPendingConfirmRows() as $row) {
+            if (!is_array($row)) {
+                continue;
+            }
+            $sid = (int)($row['scydgy_id'] ?? 0);
+            if ($sid > 0) {
+                $set[$sid] = true;
+            }
+        }
+
+        return $set;
+    }
+
+    protected static function countCompletedInRange(string $start, string $end): int
+    {
+        $startTs = ProcuremenTime::parseToTimestamp($start);
+        $endTs = ProcuremenTime::parseToTimestamp($end);
+        if ($startTs <= 0 || $endTs <= 0) {
+            return 0;
+        }
+        try {
+            $logs = Db::table('purchase_order_oper_log')
+                ->where('action', 'in', ['purchase_confirm', 'mark_complete'])
+                ->field('scydgy_id,createtime')
+                ->select();
+        } catch (\Throwable $e) {
+            return 0;
+        }
+        if (!is_array($logs) || $logs === []) {
+            return 0;
+        }
+        $sidSet = [];
+        foreach ($logs as $log) {
+            if (!is_array($log)) {
+                continue;
+            }
+            $ts = ProcuremenTime::parseToTimestamp($log['createtime'] ?? null);
+            if ($ts < $startTs || $ts > $endTs) {
+                continue;
+            }
+            $sid = (int)($log['scydgy_id'] ?? 0);
+            if ($sid > 0) {
+                $sidSet[$sid] = true;
+            }
+        }
+        if ($sidSet === []) {
+            return 0;
+        }
+
+        return self::countDistinctOrders(self::loadOrdersByScydgyIds(array_keys($sidSet)));
+    }
+
+    /**
+     * @param int[] $scydgyIds
+     * @return array<int, array<string, mixed>>
+     */
+    protected static function loadOrdersByScydgyIds(array $scydgyIds): array
+    {
+        $scydgyIds = array_values(array_unique(array_filter(array_map('intval', $scydgyIds))));
+        if ($scydgyIds === []) {
+            return [];
+        }
+        try {
+            $query = Db::table('purchase_order')
+                ->where('scydgy_id', 'in', $scydgyIds)
+                ->field('id,CCYDH,scydgy_id,wflow_status');
+            self::applyNotDeletedWhere($query);
+            $rows = $query->select();
+        } catch (\Throwable $e) {
+            return [];
+        }
+
+        return is_array($rows) ? $rows : [];
+    }
+
+    /**
+     * @return array<int, array<string, mixed>>
+     */
+    protected static function loadPendingConfirmRows(): array
+    {
+        try {
+            $query = Db::table('purchase_order')->field('id,CCYDH,scydgy_id');
+            self::applyNotDeletedWhere($query);
+            $query->whereIn('wflow_status', ProcuremenStatus::wflowPendingConfirmValues());
+            $rows = $query->select();
+        } catch (\Throwable $e) {
+            return [];
+        }
+
+        return is_array($rows) ? $rows : [];
+    }
+
+    /**
+     * @return array<int, array<string, mixed>>
+     */
+    protected static function loadPendingApprovalRows(): array
+    {
+        try {
+            $query = Db::table('purchase_order')->field('id,CCYDH');
+            self::applyNotDeletedWhere($query);
+            $query->whereIn('wflow_status', ProcuremenStatus::wflowPendingApprovalValues());
+            $query->whereRaw('NOT (' . ProcuremenStatus::sqlPoCompleted('status') . ')');
+            $rows = $query->select();
+        } catch (\Throwable $e) {
+            return [];
+        }
+
+        return is_array($rows) ? $rows : [];
+    }
+
+    /**
+     * @param array<int, array<string, mixed>> $rows
+     */
+    protected static function countDistinctOrders(array $rows): int
+    {
+        if ($rows === []) {
+            return 0;
+        }
+        $keys = [];
+        foreach ($rows as $row) {
+            if (!is_array($row)) {
+                continue;
+            }
+            $dh = trim((string)($row['CCYDH'] ?? ''));
+            $keys[$dh !== '' ? $dh : ('_id_' . (int)($row['id'] ?? 0))] = true;
+        }
+
+        return count($keys);
+    }
+
+    protected static function applyNotDeletedWhere($query): void
+    {
+        $query->whereRaw(
+            '(mod_rq IS NULL OR TRIM(CAST(mod_rq AS CHAR(32))) = \'\' OR TRIM(CAST(mod_rq AS CHAR(32))) LIKE \'0000-00-00%\')'
+        );
+    }
+
+    protected static function pickTimeSqlExpr(): string
+    {
+        $norm = function (string $col): string {
+            return "NULLIF(NULLIF(TRIM(CAST({$col} AS CHAR(32))), ''), '0000-00-00')";
+        };
+        $createtime = 'IF(createtime > 946684800, FROM_UNIXTIME(createtime), ' . $norm('createtime') . ')';
+        $parts = [
+            $norm('pick_time'),
+            $createtime,
+            $norm('dputrecord'),
+            $norm('dStamp'),
+        ];
+
+        return 'COALESCE(' . implode(', ', $parts) . ')';
+    }
+}

+ 120 - 63
public/assets/js/backend/dashboard.js

@@ -1,78 +1,135 @@
-define(['jquery', 'bootstrap', 'backend', 'addtabs', 'table', 'echarts', 'echarts-theme', 'template'], function ($, undefined, Backend, Datatable, Table, Echarts, undefined, Template) {
+define(['jquery', 'bootstrap', 'backend', 'addtabs', 'echarts', 'echarts-theme'], function ($, undefined, Backend, undefined, Echarts) {
 
     var Controller = {
         index: function () {
-            // 基于准备好的dom,初始化echarts实例
-            var myChart = Echarts.init(document.getElementById('echart'), 'walden');
-
-            // 指定图表的配置项和数据
-            var option = {
-                title: {
-                    text: '',
-                    subtext: ''
-                },
-                color: [
-                    "#18d1b1",
-                    "#3fb1e3",
-                    "#626c91",
-                    "#a0a7e6",
-                    "#c4ebad",
-                    "#96dee8"
-                ],
-                tooltip: {
-                    trigger: 'axis'
-                },
-                legend: {
-                    data: [__('Register user')]
-                },
-                toolbox: {
-                    show: false,
-                    feature: {
-                        magicType: {show: true, type: ['stack', 'tiled']},
-                        saveAsImage: {show: true}
-                    }
-                },
-                xAxis: {
-                    type: 'category',
-                    boundaryGap: false,
-                    data: Config.column
-                },
-                yAxis: {},
-                grid: [{
-                    left: 'left',
-                    top: 'top',
-                    right: '10',
-                    bottom: 30
-                }],
-                series: [{
-                    name: __('Register user'),
-                    type: 'line',
-                    smooth: true,
-                    areaStyle: {
-                        normal: {}
-                    },
-                    lineStyle: {
-                        normal: {
-                            width: 1.5
+            var procChart = null;
+            var chartData = Config.procuremenChart || {};
+            var chartEl = document.getElementById('procuremen-echart');
+            if (chartEl) {
+                procChart = Echarts.init(chartEl, 'walden');
+                var labels = chartData.labels || [];
+                var seriesMeta = [
+                    {name: '待确认', key: 'confirm', color: '#f0ad4e'},
+                    {name: '待审批', key: 'approval', color: '#7266ba'},
+                    {name: '已完结', key: 'completed', color: '#27C24C'}
+                ];
+                var maxVal = 0;
+                seriesMeta.forEach(function (meta) {
+                    (chartData[meta.key] || []).forEach(function (v) {
+                        var n = parseInt(v, 10) || 0;
+                        if (n > maxVal) {
+                            maxVal = n;
+                        }
+                    });
+                });
+                procChart.setOption({
+                    color: seriesMeta.map(function (m) {
+                        return m.color;
+                    }),
+                    title: {
+                        text: '近7天单数',
+                        left: 0,
+                        top: 0,
+                        textStyle: {
+                            fontSize: 13,
+                            fontWeight: 600,
+                            color: '#555'
                         }
                     },
-                    data: Config.userdata
-                }]
-            };
-
-            // 使用刚指定的配置项和数据显示图表。
-            myChart.setOption(option);
+                    tooltip: {
+                        trigger: 'axis',
+                        axisPointer: {type: 'line', lineStyle: {color: '#ddd'}},
+                        backgroundColor: 'rgba(50,50,50,0.92)',
+                        borderWidth: 0,
+                        textStyle: {fontSize: 12}
+                    },
+                    legend: {
+                        data: seriesMeta.map(function (m) {
+                            return m.name;
+                        }),
+                        top: 0,
+                        right: 0,
+                        itemWidth: 10,
+                        itemHeight: 10,
+                        textStyle: {fontSize: 11}
+                    },
+                    grid: {
+                        left: 2,
+                        right: 14,
+                        top: 36,
+                        bottom: 10,
+                        containLabel: true
+                    },
+                    xAxis: {
+                        type: 'category',
+                        boundaryGap: false,
+                        data: labels,
+                        axisTick: {show: false},
+                        axisLabel: {
+                            fontSize: 11,
+                            color: '#888',
+                            margin: 4,
+                            interval: 0,
+                            showMinLabel: true,
+                            showMaxLabel: true,
+                            alignMinLabel: 'left',
+                            alignMaxLabel: 'right'
+                        },
+                        axisLine: {lineStyle: {color: '#e8e8e8'}}
+                    },
+                    yAxis: {
+                        type: 'value',
+                        minInterval: 1,
+                        min: 0,
+                        max: maxVal <= 1 ? 2 : null,
+                        splitNumber: maxVal <= 2 ? 2 : 4,
+                        axisLabel: {fontSize: 11, color: '#888'},
+                        splitLine: {lineStyle: {color: '#f0f0f0', type: 'dashed'}}
+                    },
+                    series: seriesMeta.map(function (meta) {
+                        return {
+                            name: meta.name,
+                            type: 'line',
+                            smooth: true,
+                            showSymbol: true,
+                            symbol: 'circle',
+                            symbolSize: function (val) {
+                                return (parseInt(val, 10) || 0) > 0 ? 6 : 0;
+                            },
+                            lineStyle: {width: 2, color: meta.color},
+                            itemStyle: {color: meta.color, borderWidth: 2, borderColor: '#fff'},
+                            emphasis: {focus: 'series', lineStyle: {width: 3}},
+                            data: chartData[meta.key] || []
+                        };
+                    })
+                });
+            }
 
             $(window).resize(function () {
-                myChart.resize();
+                if (procChart) {
+                    procChart.resize();
+                }
             });
 
-            $(document).on("click", ".btn-refresh", function () {
+            setTimeout(function () {
+                if (procChart) {
+                    procChart.resize();
+                }
+            }, 120);
+
+            setTimeout(function () {
+                if (procChart) {
+                    procChart.resize();
+                }
+            }, 400);
+
+            $(document).on('click', '.btn-refresh', function () {
                 setTimeout(function () {
-                    myChart.resize();
+                    if (procChart) {
+                        procChart.resize();
+                    }
                 }, 0);
             });
-
         }
     };