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

feat(web-admin): 工作台图表与配色;统计今日区域/客户接口

- 控制台:近30日双折线、今日区域/客户双柱、dataZoom、竖条标题与配色
- 后端:今日按区域/按客户订单统计 API
- 小程序登录页:移除微信一键登录、缩小登录按钮
- 规范与样式:Fresh Mint 文档、el-ui、询价报价/订单页等对齐

Made-with: Cursor
zlwu пре 6 месеци
родитељ
комит
d7a49c2d82

+ 32 - 0
backend/app/api/admin.py

@@ -2231,6 +2231,7 @@ def get_selected_quotation_for_inquiry(
                 "quotationSubmitTime": None,
                 "supplierId": None,
                 "supplierName": None,
+                "supplierLogoUrl": None,
                 "items": item_details
             })
         supplier = user_crud.get(db, q.supplier_id) if q.supplier_id else None
@@ -2281,6 +2282,7 @@ def get_selected_quotation_for_inquiry(
             "quotationSubmitTime": str(q.submit_time) if getattr(q, "submit_time", None) else None,
             "supplierId": q.supplier_id,
             "supplierName": supplier.real_name if supplier else None,
+            "supplierLogoUrl": getattr(supplier, "avatar_url", None) if supplier else None,
             "items": item_details
         })
 
@@ -2362,6 +2364,7 @@ def get_selected_quotation_for_inquiry(
         "quotationSubmitTime": quotation_submit_time,
         "supplierId": pl.supplier_id,
         "supplierName": supplier.real_name if supplier else None,
+        "supplierLogoUrl": getattr(supplier, "avatar_url", None) if supplier else None,
         "items": item_details
     })
 
@@ -6714,6 +6717,35 @@ def get_order_trend(
     return success_response(trend)
 
 
+@router.get("/statistics/orders/today-by-region", response_model=dict)
+def get_today_orders_by_region(
+    regionId: Optional[str] = Query(None, description="区域ID,传入时仅返回该区域"),
+    db: Session = Depends(get_db),
+    current_user: dict = Depends(require_admin)
+):
+    """今日各区域营业额(元)与订单数(按区域列表,无单为 0)"""
+    from app.crud.statistics import statistics_crud
+
+    rows = statistics_crud.get_today_order_stats_by_region(db, region_id=regionId)
+    return success_response(rows)
+
+
+@router.get("/statistics/orders/today-by-client", response_model=dict)
+def get_today_orders_by_client(
+    regionId: Optional[str] = Query(None, description="区域ID"),
+    limit: int = Query(30, ge=1, le=100, description="最多返回客户条数"),
+    db: Session = Depends(get_db),
+    current_user: dict = Depends(require_admin)
+):
+    """今日下单客户:下单金额(元)、商品数量合计(按客户,金额降序)"""
+    from app.crud.statistics import statistics_crud
+
+    rows = statistics_crud.get_today_order_stats_by_client(
+        db, region_id=regionId, limit=limit
+    )
+    return success_response(rows)
+
+
 @router.get("/statistics/financial", response_model=dict)
 def get_financial_statistics(
     regionId: Optional[str] = Query(None, description="区域ID"),

+ 378 - 16
backend/app/crud/statistics.py

@@ -8,7 +8,8 @@ from decimal import Decimal
 from app.models.models import (
     BizOrder, BizOrderItem, BizDeliveryTask,
     BizSettlement, BizRefund, BizPriceList, BizPriceListItem,
-    SysUser, SysRegion, BasProduct
+    SysUser, SysRegion, BasProduct,
+    BizInquiry, BizQuotation, BizDeliveryWarning,
 )
 
 
@@ -105,6 +106,138 @@ class StatisticsCRUD:
 
         return result
 
+    @staticmethod
+    def get_today_order_stats_by_region(
+        db: Session,
+        region_id: str = None,
+    ) -> List[Dict[str, Any]]:
+        """今日各区域营业额(元)、订单笔数;列出未删除且启用的区域,无单则为 0。"""
+        today = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
+        day_end = today + timedelta(days=1)
+
+        reg_q = db.query(SysRegion).filter(
+            SysRegion.is_del == 0,
+            SysRegion.is_active == True,
+        )
+        if region_id:
+            reg_q = reg_q.filter(SysRegion.id == region_id)
+        regions = reg_q.order_by(SysRegion.sort_order, SysRegion.name).all()
+
+        agg_q = (
+            db.query(
+                BizOrder.region_id,
+                func.count(BizOrder.id).label("order_count"),
+                func.coalesce(func.sum(BizOrder.total_amount), 0).label("order_amount"),
+            )
+            .filter(
+                BizOrder.is_del == 0,
+                BizOrder.create_at >= today,
+                BizOrder.create_at < day_end,
+            )
+            .group_by(BizOrder.region_id)
+        )
+        if region_id:
+            agg_q = agg_q.filter(BizOrder.region_id == region_id)
+        agg_map: Dict[str, Any] = {r.region_id: r for r in agg_q.all() if r.region_id}
+
+        out: List[Dict[str, Any]] = []
+        for reg in regions:
+            row = agg_map.get(reg.id)
+            cnt = int(row.order_count or 0) if row else 0
+            amt = float(row.order_amount or 0) if row else 0.0
+            out.append({
+                "regionId": reg.id,
+                "regionName": reg.name or reg.id,
+                "orderCount": cnt,
+                "orderAmount": round(amt, 2),
+            })
+        return out
+
+    @staticmethod
+    def get_today_order_stats_by_client(
+        db: Session,
+        region_id: str = None,
+        limit: int = 30,
+    ) -> List[Dict[str, Any]]:
+        """今日各下单客户:下单金额合计(元)、商品数量合计(订单行数量之和);按金额降序,最多 limit 条。"""
+        today = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
+        day_end = today + timedelta(days=1)
+
+        amt_q = (
+            db.query(
+                BizOrder.client_id,
+                func.coalesce(func.sum(BizOrder.total_amount), 0).label("order_amount"),
+            )
+            .filter(
+                BizOrder.is_del == 0,
+                BizOrder.create_at >= today,
+                BizOrder.create_at < day_end,
+            )
+        )
+        if region_id:
+            amt_q = amt_q.filter(BizOrder.region_id == region_id)
+        amt_rows = amt_q.group_by(BizOrder.client_id).all()
+
+        qty_q = (
+            db.query(
+                BizOrder.client_id,
+                func.coalesce(func.sum(BizOrderItem.quantity), 0).label("qty"),
+            )
+            .select_from(BizOrderItem)
+            .join(
+                BizOrder,
+                and_(
+                    BizOrder.id == BizOrderItem.order_id,
+                    BizOrder.is_del == 0,
+                    BizOrder.create_at >= today,
+                    BizOrder.create_at < day_end,
+                ),
+            )
+            .filter(BizOrderItem.is_del == 0)
+        )
+        if region_id:
+            qty_q = qty_q.filter(BizOrder.region_id == region_id)
+        qty_rows = qty_q.group_by(BizOrder.client_id).all()
+        qty_map: Dict[str, float] = {
+            r.client_id: float(r.qty or 0) for r in qty_rows if r.client_id
+        }
+
+        merged: List[Dict[str, Any]] = []
+        for r in amt_rows:
+            cid = r.client_id
+            if not cid:
+                continue
+            merged.append({
+                "clientId": cid,
+                "orderAmount": round(float(r.order_amount or 0), 2),
+                "productQuantity": round(float(qty_map.get(cid, 0) or 0), 4),
+            })
+
+        merged.sort(key=lambda x: -x["orderAmount"])
+        merged = merged[: max(1, min(int(limit or 30), 100))]
+
+        client_ids = [m["clientId"] for m in merged]
+        names: Dict[str, str] = {}
+        if client_ids:
+            for u in (
+                db.query(SysUser)
+                .filter(and_(SysUser.is_del == 0, SysUser.id.in_(client_ids)))
+                .all()
+            ):
+                names[u.id] = (u.real_name or u.phone or u.username or u.id) or u.id
+
+        out: List[Dict[str, Any]] = []
+        for m in merged:
+            cid = m["clientId"]
+            nm = names.get(cid, cid)
+            out.append({
+                "clientId": cid,
+                "clientName": nm,
+                "orderAmount": m["orderAmount"],
+                "productQuantity": m["productQuantity"],
+            })
+        return out
+
     @staticmethod
     def get_financial_statistics(
         db: Session,
@@ -373,40 +506,264 @@ class StatisticsCRUD:
 
         return customer_stats
 
+    @staticmethod
+    def _dashboard_order_region_clause(region_id: str = None):
+        return [BizOrder.region_id == region_id] if region_id else []
+
     @staticmethod
     def get_dashboard_summary(
         db: Session,
         region_id: str = None
     ) -> Dict[str, Any]:
-        """获取仪表盘汇总数据"""
+        """获取仪表盘汇总数据(运营工作台;含待办摘要、最新订单、预警数等,字段驼峰)"""
         today = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
         month_start = today.replace(day=1)
+        yesterday_start = today - timedelta(days=1)
+        region_clause = StatisticsCRUD._dashboard_order_region_clause(region_id)
 
         order_query = db.query(BizOrder).filter(
-            and_(BizOrder.is_del == 0, BizOrder.create_at >= month_start)
+            and_(BizOrder.is_del == 0, BizOrder.create_at >= month_start, *region_clause)
         )
-        if region_id:
-            order_query = order_query.filter(BizOrder.region_id == region_id)
-
         month_orders = order_query.all()
-        today_orders = [o for o in month_orders if o.create_at >= today]
-
-        pending_delivery_count = db.query(BizOrder).filter(
+        today_orders = [
+            o for o in month_orders if o.create_at and o.create_at >= today
+        ]
+        prev_day_orders = [
+            o
+            for o in month_orders
+            if o.create_at and yesterday_start <= o.create_at < today
+        ]
+
+        pending_delivery_q = db.query(BizOrder).filter(
             and_(
                 BizOrder.is_del == 0,
-                BizOrder.delivery_status == "pending_dispatch"
+                BizOrder.delivery_status == "pending_dispatch",
+                *region_clause,
+            )
+        )
+        pending_delivery_count = pending_delivery_q.count()
+
+        def _delta_hint(cur_n: int, prev_n: int, unit: str) -> str:
+            d = cur_n - prev_n
+            if d > 0:
+                return f"较昨日 +{d}{unit}"
+            if d < 0:
+                return f"较昨日 {d}{unit}"
+            return "与昨日持平"
+
+        def _amount_delta_hint(cur_amt: float, prev_amt: float) -> str:
+            d = round(cur_amt - prev_amt, 2)
+            if d > 0.0001:
+                return f"较昨日 +¥{d:,.2f}"
+            if d < -0.0001:
+                return f"较昨日 ¥{d:,.2f}"
+            return "与昨日持平"
+
+        today_order_count = len(today_orders)
+        today_order_amount = round(sum(float(o.total_amount or 0) for o in today_orders), 2)
+        prev_count = len(prev_day_orders)
+        prev_amount = round(sum(float(o.total_amount or 0) for o in prev_day_orders), 2)
+
+        # 未解决的配送时效预警(可按区域通过任务表关联)
+        warn_q = db.query(BizDeliveryWarning).filter(
+            and_(BizDeliveryWarning.is_del == 0, BizDeliveryWarning.is_resolved == 0)
+        )
+        if region_id:
+            warn_q = warn_q.join(
+                BizDeliveryTask,
+                and_(
+                    BizDeliveryTask.id == BizDeliveryWarning.task_id,
+                    BizDeliveryTask.is_del == 0,
+                    BizDeliveryTask.region_id == region_id,
+                ),
             )
+        delivery_warning_count = warn_q.count()
+
+        # 待处理报价:已提交、待运营侧处理的报价单
+        sub_quot_q = db.query(BizQuotation).filter(
+            and_(BizQuotation.is_del == 0, BizQuotation.status == "submitted")
         )
         if region_id:
-            pending_delivery_count = pending_delivery_count.filter(BizOrder.region_id == region_id)
+            sub_quot_q = sub_quot_q.filter(BizQuotation.region_id == region_id)
+        pending_quotation_count = sub_quot_q.count()
+
+        sub_quot_preview = (
+            db.query(BizQuotation, BizInquiry)
+            .join(BizInquiry, BizInquiry.id == BizQuotation.inquiry_id)
+            .filter(
+                BizQuotation.is_del == 0,
+                BizQuotation.status == "submitted",
+                BizInquiry.is_del == 0,
+                *([BizQuotation.region_id == region_id] if region_id else []),
+            )
+            .order_by(desc(BizQuotation.submit_time), desc(BizQuotation.create_at))
+            .limit(2)
+            .all()
+        )
+        quotation_lines = []
+        for q, inq in sub_quot_preview:
+            title = (inq.title or "询价单")[:48]
+            quotation_lines.append(title)
+
+        pd_preview = (
+            db.query(BizOrder)
+            .filter(
+                and_(
+                    BizOrder.is_del == 0,
+                    BizOrder.delivery_status == "pending_dispatch",
+                    *region_clause,
+                )
+            )
+            .order_by(desc(BizOrder.create_at))
+            .limit(2)
+            .all()
+        )
+        delivery_lines = [o.order_no for o in pd_preview]
+
+        cancel_preview = (
+            db.query(BizOrder)
+            .filter(
+                and_(BizOrder.is_del == 0, BizOrder.cancel_status == "pending_review", *region_clause)
+            )
+            .order_by(desc(BizOrder.update_at))
+            .limit(2)
+            .all()
+        )
+        cancel_client_ids = list({o.client_id for o in cancel_preview if o.client_id})
+        cancel_clients = {}
+        if cancel_client_ids:
+            for u in (
+                db.query(SysUser)
+                .filter(and_(SysUser.is_del == 0, SysUser.id.in_(cancel_client_ids)))
+                .all()
+            ):
+                cancel_clients[u.id] = u.real_name or u.phone or u.id
+        cancel_lines = []
+        for o in cancel_preview:
+            nm = cancel_clients.get(o.client_id, "")
+            cancel_lines.append(f"{o.order_no}" + (f" · {nm[:16]}" if nm else ""))
+
+        settle_preview = (
+            db.query(BizOrder)
+            .filter(
+                and_(
+                    BizOrder.is_del == 0,
+                    BizOrder.client_settlement_status == "pending",
+                    BizOrder.pay_status == "paid",
+                    BizOrder.order_status.in_(["pending_confirm", "completed"]),
+                    *region_clause,
+                )
+            )
+            .order_by(desc(BizOrder.update_at))
+            .limit(2)
+            .all()
+        )
+        settle_client_ids = list({o.client_id for o in settle_preview if o.client_id})
+        settle_clients = {}
+        if settle_client_ids:
+            for u in (
+                db.query(SysUser)
+                .filter(and_(SysUser.is_del == 0, SysUser.id.in_(settle_client_ids)))
+                .all()
+            ):
+                settle_clients[u.id] = u.real_name or u.phone or u.id
+        settle_lines = []
+        for o in settle_preview:
+            nm = settle_clients.get(o.client_id, "")
+            settle_lines.append(f"{o.order_no}" + (f" · {nm[:16]}" if nm else ""))
+
+        pending_settlement_count = (
+            db.query(BizOrder)
+            .filter(
+                and_(
+                    BizOrder.is_del == 0,
+                    BizOrder.client_settlement_status == "pending",
+                    BizOrder.pay_status == "paid",
+                    BizOrder.order_status.in_(["pending_confirm", "completed"]),
+                    *region_clause,
+                )
+            )
+            .count()
+        )
+
+        pending_cancel_count = (
+            db.query(BizOrder)
+            .filter(and_(BizOrder.is_del == 0, BizOrder.cancel_status == "pending_review", *region_clause))
+            .count()
+        )
+
+        # 最新订单(工作台列表)
+        recent_rows = (
+            db.query(BizOrder)
+            .filter(and_(BizOrder.is_del == 0, *region_clause))
+            .order_by(desc(BizOrder.create_at))
+            .limit(8)
+            .all()
+        )
+        rid_set = list({o.region_id for o in recent_rows if o.region_id})
+        cid_set = list({o.client_id for o in recent_rows if o.client_id})
+        region_name_map = {}
+        if rid_set:
+            for r in db.query(SysRegion).filter(and_(SysRegion.is_del == 0, SysRegion.id.in_(rid_set))).all():
+                region_name_map[r.id] = r.name
+        client_name_map = {}
+        if cid_set:
+            for u in db.query(SysUser).filter(and_(SysUser.is_del == 0, SysUser.id.in_(cid_set))).all():
+                client_name_map[u.id] = u.real_name or u.phone or "-"
+
+        recent_orders = []
+        for o in recent_rows:
+            order_status = getattr(o, "order_status", None) or "pending_payment"
+            recent_orders.append(
+                {
+                    "id": o.id,
+                    "orderNo": o.order_no,
+                    "clientName": client_name_map.get(o.client_id, "-"),
+                    "regionName": region_name_map.get(o.region_id, "-"),
+                    "totalAmount": float(o.total_amount) if o.total_amount else 0,
+                    "orderStatus": order_status,
+                    "cancelStatus": getattr(o, "cancel_status", None) or "none",
+                    "deliveryStatus": getattr(o, "delivery_status", None) or "pending_dispatch",
+                    "createAt": str(o.create_at) if o.create_at else None,
+                }
+            )
+
+        todo_snapshots = [
+            {
+                "type": "quotation",
+                "title": "待处理报价",
+                "count": pending_quotation_count,
+                "items": quotation_lines,
+            },
+            {
+                "type": "delivery",
+                "title": "待分配配送",
+                "count": pending_delivery_count,
+                "items": delivery_lines,
+            },
+            {
+                "type": "cancel",
+                "title": "取消待审核",
+                "count": pending_cancel_count,
+                "items": cancel_lines,
+            },
+            {
+                "type": "settlement",
+                "title": "待客户结算",
+                "count": pending_settlement_count,
+                "items": settle_lines,
+            },
+        ]
 
         return {
-            "todayOrderCount": len(today_orders),
-            "todayOrderAmount": round(sum(float(o.total_amount or 0) for o in today_orders), 2),
+            "todayOrderCount": today_order_count,
+            "todayOrderAmount": today_order_amount,
             "monthOrderCount": len(month_orders),
             "monthOrderAmount": round(sum(float(o.total_amount or 0) for o in month_orders), 2),
-            "pendingDeliveryCount": pending_delivery_count.count(),
-            "completedOrderCount": sum(1 for o in month_orders if o.status == "completed"),
+            "pendingDeliveryCount": pending_delivery_count,
+            "completedOrderCount": sum(
+                1 for o in month_orders if (getattr(o, "order_status", None) or o.status) == "completed"
+            ),
             "activePriceListCount": db.query(BizPriceList).filter(
                 and_(BizPriceList.is_del == 0, BizPriceList.status == "active")
             ).count(),
@@ -415,7 +772,12 @@ class StatisticsCRUD:
             ).count(),
             "supplierCount": db.query(SysUser).filter(
                 and_(SysUser.is_del == 0, SysUser.role == "supplier")
-            ).count()
+            ).count(),
+            "todayOrderCountHint": _delta_hint(today_order_count, prev_count, " 单"),
+            "todayOrderAmountHint": _amount_delta_hint(today_order_amount, prev_amount),
+            "deliveryWarningCount": delivery_warning_count,
+            "recentOrders": recent_orders,
+            "todoSnapshots": todo_snapshots,
         }
 
 

Разлика између датотеке није приказан због своје велике величине
+ 0 - 1
docs/web-admin-ui-spec-fresh-mint.md


+ 3 - 89
miniprogram/src/pages/login/login.vue

@@ -36,27 +36,6 @@ const rules = {
 // 登录状态
 const isLoading = ref(false)
 
-// 微信登录
-const handleWxLogin = async () => {
-  try {
-    // 获取微信授权码
-    const loginRes = await uni.login({ provider: 'weixin' })
-
-    if (loginRes.code) {
-      // 调用微信登录接口
-      isLoading.value = true
-      const success = await userStore.wxLogin(loginRes.code)
-
-      if (success) {
-        userStore.navigateToHome()
-      }
-    }
-  } catch (error) {
-    console.error('微信登录失败:', error)
-    uni.showToast({ title: '微信登录失败', icon: 'none' })
-  }
-}
-
 // 表单提交
 const handleSubmit = async () => {
   // 简单验证
@@ -158,17 +137,6 @@ const goPrivacyPolicy = () => {
             <text v-if="isLoading" class="loading-text">登录中...</text>
             <text v-else>账号密码登录</text>
           </button>
-
-          <view class="divider-text">
-            <view class="divider-line"></view>
-            <text class="divider-text-content">或</text>
-            <view class="divider-line"></view>
-          </view>
-
-          <button class="btn-wx-login" @click="handleWxLogin">
-            <text class="wx-icon">微信</text>
-            <text>微信一键登录</text>
-          </button>
         </view>
       </view>
 
@@ -321,10 +289,10 @@ const goPrivacyPolicy = () => {
 .form-actions {
   .btn-login {
     width: 100%;
-    height: 96rpx;
+    height: 76rpx;
     background: linear-gradient(135deg, var(--theme-color) 0%, #07c160 100%);
-    border-radius: 48rpx;
-    font-size: 32rpx;
+    border-radius: 38rpx;
+    font-size: 28rpx;
     font-weight: 500;
     color: #ffffff;
     display: flex;
@@ -345,60 +313,6 @@ const goPrivacyPolicy = () => {
       opacity: 0.7;
     }
   }
-
-  .divider-text {
-    display: flex;
-    align-items: center;
-    margin: 40rpx 0;
-    padding: 0 20rpx;
-
-    .divider-line {
-      flex: 1;
-      height: 1rpx;
-      background: #e4e7ed;
-    }
-
-    .divider-text-content {
-      padding: 0 24rpx;
-      font-size: 24rpx;
-      color: #909399;
-    }
-  }
-
-  .btn-wx-login {
-    width: 100%;
-    height: 96rpx;
-    background: #f5f7fa;
-    border-radius: 48rpx;
-    font-size: 32rpx;
-    font-weight: 500;
-    color: #303133;
-    display: flex;
-    align-items: center;
-    justify-content: center;
-    border: none;
-
-    &::after {
-      border: none;
-    }
-
-    &:active {
-      background: #ebeef5;
-    }
-
-    .wx-icon {
-      width: 48rpx;
-      height: 48rpx;
-      background: #07c160;
-      border-radius: 50%;
-      color: #fff;
-      font-size: 20rpx;
-      display: flex;
-      align-items: center;
-      justify-content: center;
-      margin-right: 12rpx;
-    }
-  }
 }
 
 // 底部协议

+ 1 - 1
web-admin/src/App.vue

@@ -78,7 +78,7 @@
   const panelTitleClassList = panelTitleIconByText.map(([, cls]) => cls)
   const panelTitleBarClass = 'title-bar-heading'
   const panelTitleBarTextRules: RegExp[] = [
-    /(分类列表|单位列表|商品列表|参数列表|用户列表|区域列表|区域与询报价概览|报价管理|订单列表|待结算订单|结算单记录|配送任务列表|预警列表|订单设置|客服设置|请求列表)/
+    /(数据概览|快捷菜单|分类列表|单位列表|商品列表|参数列表|用户列表|区域列表|区域与询报价概览|报价管理|订单列表|最新订单|待结算订单|结算单记录|配送任务列表|预警列表|订单设置|客服设置|请求列表|近30日趋势|今日各区域)/
   ]
 
   const refreshPanelTitleIcons = () => {

+ 93 - 0
web-admin/src/api/business.ts

@@ -806,6 +806,39 @@ export function fetchProcessRefund(data: { refundId: string; approved: boolean;
 // 统计管理 API
 // =========================================
 
+/** 运营工作台 / 仪表盘接口返回(与后端 success data 一致,驼峰) */
+export interface WorkbenchDashboardData {
+  todayOrderCount: number
+  todayOrderAmount: number
+  monthOrderCount: number
+  monthOrderAmount: number
+  pendingDeliveryCount: number
+  completedOrderCount: number
+  activePriceListCount: number
+  customerCount: number
+  supplierCount: number
+  todayOrderCountHint?: string
+  todayOrderAmountHint?: string
+  deliveryWarningCount?: number
+  recentOrders?: Array<{
+    id: string
+    orderNo: string
+    clientName: string
+    regionName: string
+    totalAmount: number
+    orderStatus: string
+    cancelStatus?: string
+    deliveryStatus?: string
+    createAt?: string | null
+  }>
+  todoSnapshots?: Array<{
+    type: string
+    title: string
+    count: number
+    items: string[]
+  }>
+}
+
 /**
  * 获取统计数据
  */
@@ -821,6 +854,66 @@ export function fetchStatistics(params: {
   })
 }
 
+/** 运营工作台专用:同 statistics/dashboard,带类型 */
+export function fetchWorkbenchDashboard(params?: { regionId?: string }) {
+  return request.get<WorkbenchDashboardData>({
+    url: '/web/admin/statistics/dashboard',
+    params
+  })
+}
+
+/** 按日订单趋势(与后端 `get_order_trend` 一致;金额单位:元) */
+export interface OrderTrendDayItem {
+  date: string
+  orderCount: number
+  orderAmount: number
+}
+
+export function fetchOrderTrend(params?: { regionId?: string; days?: number }) {
+  return request.get<OrderTrendDayItem[]>({
+    url: '/web/admin/statistics/orders/trend',
+    params: {
+      regionId: params?.regionId,
+      days: params?.days ?? 30
+    }
+  })
+}
+
+/** 今日各区域订单汇总(金额单位:元) */
+export interface TodayRegionOrderStatItem {
+  regionId: string
+  regionName: string
+  orderCount: number
+  orderAmount: number
+}
+
+export function fetchTodayOrdersByRegion(params?: { regionId?: string }) {
+  return request.get<TodayRegionOrderStatItem[]>({
+    url: '/web/admin/statistics/orders/today-by-region',
+    params: {
+      regionId: params?.regionId
+    }
+  })
+}
+
+/** 今日下单客户:金额(元) + 商品数量合计 */
+export interface TodayClientOrderStatItem {
+  clientId: string
+  clientName: string
+  orderAmount: number
+  productQuantity: number
+}
+
+export function fetchTodayOrdersByClient(params?: { regionId?: string; limit?: number }) {
+  return request.get<TodayClientOrderStatItem[]>({
+    url: '/web/admin/statistics/orders/today-by-client',
+    params: {
+      regionId: params?.regionId,
+      limit: params?.limit ?? 30
+    }
+  })
+}
+
 /**
  * 获取 API 请求日志(中间件记录全量出入参)
  */

+ 34 - 5
web-admin/src/assets/styles/core/el-ui.scss

@@ -958,11 +958,40 @@ body .el-table thead {
   background: var(--mint-surface);
 }
 
-.page-container .el-table__cell .btn-op-item.el-button:first-of-type,
-.console-container .el-table__cell .btn-op-item.el-button:first-of-type {
-  color: #fff;
-  border-color: #d1ac44;
-  background: #d1ac44;
+/* 列表操作列第一个按钮:白底 + 主色字(与各页「查看」等首操作一致;排除 link/text 与警示类) */
+.page-container .el-table__cell .btn-op-item.el-button:first-of-type:not(.el-button--link):not(.el-button--text):not(.op-warn),
+.console-container .el-table__cell .btn-op-item.el-button:first-of-type:not(.el-button--link):not(.el-button--text):not(.op-warn),
+.el-dialog .el-table__cell .btn-op-item.el-button:first-of-type:not(.el-button--link):not(.el-button--text):not(.op-warn) {
+  background: #fff !important;
+  border: 1px solid var(--mint-border) !important;
+  color: var(--mint-primary-dark) !important;
+  box-shadow: none !important;
+}
+
+.page-container .el-table__cell .btn-op-item.el-button:first-of-type:not(.el-button--link):not(.el-button--text):not(.op-warn) .el-icon,
+.console-container .el-table__cell .btn-op-item.el-button:first-of-type:not(.el-button--link):not(.el-button--text):not(.op-warn) .el-icon,
+.el-dialog .el-table__cell .btn-op-item.el-button:first-of-type:not(.el-button--link):not(.el-button--text):not(.op-warn) .el-icon {
+  color: var(--mint-primary-dark) !important;
+}
+
+.page-container .el-table__cell .btn-op-item.el-button:first-of-type:not(.el-button--link):not(.el-button--text):not(.op-warn):hover,
+.page-container .el-table__cell .btn-op-item.el-button:first-of-type:not(.el-button--link):not(.el-button--text):not(.op-warn):focus-visible,
+.console-container .el-table__cell .btn-op-item.el-button:first-of-type:not(.el-button--link):not(.el-button--text):not(.op-warn):hover,
+.console-container .el-table__cell .btn-op-item.el-button:first-of-type:not(.el-button--link):not(.el-button--text):not(.op-warn):focus-visible,
+.el-dialog .el-table__cell .btn-op-item.el-button:first-of-type:not(.el-button--link):not(.el-button--text):not(.op-warn):hover,
+.el-dialog .el-table__cell .btn-op-item.el-button:first-of-type:not(.el-button--link):not(.el-button--text):not(.op-warn):focus-visible {
+  background: var(--mint-primary-bg) !important;
+  border-color: var(--mint-primary) !important;
+  color: #047857 !important;
+}
+
+.page-container .el-table__cell .btn-op-item.el-button:first-of-type:not(.el-button--link):not(.el-button--text):not(.op-warn):hover .el-icon,
+.page-container .el-table__cell .btn-op-item.el-button:first-of-type:not(.el-button--link):not(.el-button--text):not(.op-warn):focus-visible .el-icon,
+.console-container .el-table__cell .btn-op-item.el-button:first-of-type:not(.el-button--link):not(.el-button--text):not(.op-warn):hover .el-icon,
+.console-container .el-table__cell .btn-op-item.el-button:first-of-type:not(.el-button--link):not(.el-button--text):not(.op-warn):focus-visible .el-icon,
+.el-dialog .el-table__cell .btn-op-item.el-button:first-of-type:not(.el-button--link):not(.el-button--text):not(.op-warn):hover .el-icon,
+.el-dialog .el-table__cell .btn-op-item.el-button:first-of-type:not(.el-button--link):not(.el-button--text):not(.op-warn):focus-visible .el-icon {
+  color: #047857 !important;
 }
 
 // 列表分割线统一:每个分区只保留一条线

+ 268 - 0
web-admin/src/views/dashboard/console/RegionTodayDualBarChart.vue

@@ -0,0 +1,268 @@
+<!-- 双指标分组柱状图:左轴金额、右轴数量(区域:营业额+订单笔;客户:下单金额+商品数量) -->
+<template>
+  <div ref="wrapRef" class="region-today-dual-bar" :style="{ height }" v-loading="loading"></div>
+</template>
+
+<script setup lang="ts">
+  import { ref, watch, onMounted, onBeforeUnmount, nextTick } from 'vue'
+  import { echarts, type EChartsOption } from '@/plugins/echarts'
+
+  defineOptions({ name: 'RegionTodayDualBarChart' })
+
+  const props = withDefaults(
+    defineProps<{
+      categories: string[]
+      amounts: number[]
+      counts: number[]
+      loading?: boolean
+      height?: string
+      /** 空数据提示 */
+      emptyText?: string
+      /** 左柱系列名(图例) */
+      amountSeriesName?: string
+      /** 右柱系列名(图例) */
+      countSeriesName?: string
+      /** 左 Y 轴名称 */
+      leftAxisName?: string
+      /** 右 Y 轴名称 */
+      rightAxisName?: string
+      /** 右轴数值在 tooltip 中的后缀,如 笔 / 件 */
+      countValueSuffix?: string
+      /** 左柱(金额类)颜色 */
+      amountColor?: string
+      /** 右柱(数量类)颜色 */
+      countColor?: string
+    }>(),
+    {
+      loading: false,
+      height: '320px',
+      emptyText: '暂无数据',
+      amountSeriesName: '营业额(元)',
+      countSeriesName: '订单数(笔)',
+      leftAxisName: '元',
+      rightAxisName: '笔',
+      countValueSuffix: '笔',
+      amountColor: '#10b981',
+      countColor: '#64748b'
+    }
+  )
+
+  const wrapRef = ref<HTMLElement | null>(null)
+  let chartInst: echarts.ECharts | null = null
+
+  const textMuted = '#64748b'
+
+  const disposeChart = () => {
+    chartInst?.dispose()
+    chartInst = null
+  }
+
+  const ensureChart = () => {
+    if (!wrapRef.value) return null
+    if (!chartInst) chartInst = echarts.init(wrapRef.value)
+    return chartInst
+  }
+
+  const applyOption = () => {
+    const chart = ensureChart()
+    if (!chart) return
+
+    const {
+      categories,
+      amounts,
+      counts,
+      emptyText,
+      amountSeriesName,
+      countSeriesName,
+      leftAxisName,
+      rightAxisName,
+      countValueSuffix,
+      amountColor,
+      countColor
+    } = props
+
+    if (!categories.length) {
+      chart.setOption(
+        {
+          title: {
+            text: emptyText,
+            left: 'center',
+            top: 'middle',
+            textStyle: { color: textMuted, fontSize: 14 }
+          },
+          xAxis: [{ show: false }],
+          yAxis: [{ show: false }],
+          dataZoom: [],
+          series: []
+        },
+        true
+      )
+      return
+    }
+
+    const n = categories.length
+    /** 类目较多时启用底部滑块 + 内置平移,避免 X 轴挤在一起 */
+    const needDataZoom = n > 10
+    const visibleBars = 12
+    /** 初始窗口约展示 visibleBars 个类目(仅当 n 明显多于窗口时缩小 end) */
+    const initialEndPercent =
+      needDataZoom && n > visibleBars ? (visibleBars / n) * 100 : 100
+
+    const option: EChartsOption = {
+      title: { show: false },
+      color: [amountColor, countColor],
+      tooltip: {
+        trigger: 'axis',
+        axisPointer: { type: 'shadow' },
+        formatter: (params: unknown) => {
+          const list = params as Array<{
+            axisValueLabel?: string
+            seriesName?: string
+            marker?: string
+            data?: number
+          }>
+          if (!list?.length) return ''
+          const head = list[0]?.axisValueLabel ?? ''
+          const lines = list.map((p) => {
+            const v = Number(p.data ?? 0)
+            const name = String(p.seriesName || '')
+            const isAmt = /元|金额/.test(name)
+            const val = isAmt
+              ? `¥${v.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`
+              : `${v.toLocaleString('zh-CN', { maximumFractionDigits: 4 })} ${countValueSuffix}`
+            return `${p.marker || ''}${p.seriesName || ''}:${val}`
+          })
+          return [head, ...lines].join('<br/>')
+        }
+      },
+      legend: {
+        data: [amountSeriesName, countSeriesName],
+        bottom: needDataZoom ? 34 : 4,
+        textStyle: { color: textMuted, fontSize: 12 }
+      },
+      grid: {
+        left: 12,
+        right: 12,
+        top: 28,
+        bottom: needDataZoom ? 82 : 48,
+        containLabel: true
+      },
+      dataZoom: needDataZoom
+        ? [
+            {
+              type: 'slider',
+              xAxisIndex: 0,
+              bottom: 4,
+              height: 22,
+              start: 0,
+              end: initialEndPercent,
+              borderColor: '#d9e8e2',
+              backgroundColor: '#f8fafc',
+              fillerColor: 'rgba(16, 185, 129, 0.12)',
+              handleStyle: { color: '#10b981', borderColor: '#10b981' },
+              moveHandleStyle: { color: '#059669' },
+              textStyle: { color: textMuted, fontSize: 11 },
+              brushSelect: false
+            },
+            {
+              type: 'inside',
+              xAxisIndex: 0,
+              zoomOnMouseWheel: false,
+              moveOnMouseMove: true,
+              moveOnMouseWheel: true
+            }
+          ]
+        : [],
+      xAxis: {
+        type: 'category',
+        data: categories,
+        axisLabel: {
+          color: textMuted,
+          fontSize: n > 20 ? 10 : 12,
+          interval: 0,
+          rotate: needDataZoom ? 28 : n > 8 ? 24 : n > 5 ? 18 : 0,
+          hideOverlap: true
+        },
+        axisLine: { lineStyle: { color: '#d9e8e2' } }
+      },
+      yAxis: [
+        {
+          type: 'value',
+          name: leftAxisName,
+          position: 'left',
+          nameTextStyle: { color: amountColor, fontSize: 12 },
+          axisLabel: {
+            color: textMuted,
+            formatter: (val: number) =>
+              val >= 10000 ? `${(val / 10000).toFixed(1)}万` : String(val)
+          },
+          splitLine: { lineStyle: { color: '#e2e8f0', type: 'dashed' } }
+        },
+        {
+          type: 'value',
+          name: rightAxisName,
+          position: 'right',
+          nameTextStyle: { color: countColor, fontSize: 12 },
+          axisLabel: { color: textMuted },
+          splitLine: { show: false },
+          minInterval: countValueSuffix === '笔' ? 1 : undefined
+        }
+      ],
+      series: [
+        {
+          name: amountSeriesName,
+          type: 'bar',
+          yAxisIndex: 0,
+          data: amounts,
+          barMaxWidth: n > 16 ? 22 : 32,
+          barCategoryGap: n > 12 ? '35%' : '40%',
+          itemStyle: {
+            borderRadius: [6, 6, 0, 0],
+            color: amountColor
+          }
+        },
+        {
+          name: countSeriesName,
+          type: 'bar',
+          yAxisIndex: 1,
+          data: counts,
+          barMaxWidth: n > 16 ? 22 : 32,
+          barCategoryGap: n > 12 ? '35%' : '40%',
+          itemStyle: {
+            borderRadius: [6, 6, 0, 0],
+            color: countColor
+          }
+        }
+      ]
+    }
+    chart.setOption(option, true)
+    chart.resize()
+  }
+
+  const onResize = () => {
+    chartInst?.resize()
+  }
+
+  onMounted(() => {
+    window.addEventListener('resize', onResize)
+    nextTick(() => applyOption())
+  })
+
+  watch(
+    () => props,
+    () => nextTick(() => applyOption()),
+    { deep: true }
+  )
+
+  onBeforeUnmount(() => {
+    window.removeEventListener('resize', onResize)
+    disposeChart()
+  })
+</script>
+
+<style scoped>
+  .region-today-dual-bar {
+    width: 100%;
+    min-height: 200px;
+  }
+</style>

+ 663 - 423
web-admin/src/views/dashboard/console/index.vue

@@ -9,87 +9,182 @@
     <!-- 预警提示 -->
     <div v-if="warningCount > 0" class="alert-bar" @click="goToWarning">
       <el-icon class="alert-icon"><WarningFilled /></el-icon>
-      <span class="alert-text">您有 <strong>{{ warningCount }}</strong> 个配送单即将超时,请及时处理!</span>
+      <span class="alert-text">
+        您有 <strong>{{ warningCount }}</strong> 条配送时效预警未处理,请及时跟进!
+      </span>
       <span class="alert-btn">立即处理</span>
     </div>
 
-    <!-- 数据统计卡片 -->
-    <el-row :gutter="16" class="stats-row">
-      <el-col :xs="12" :sm="8" :md="5" v-for="stat in statsData" :key="stat.key">
-        <div class="stats-card" @click="handleStatClick(stat.key)">
-          <div class="stats-icon" :class="stat.colorClass">
-            <el-icon :size="24"><component :is="stat.icon" /></el-icon>
-          </div>
-          <div class="stats-info">
-            <div class="stats-num">{{ stat.value }}</div>
-            <div class="stats-label">{{ stat.label }}</div>
+    <!-- 指标卡:7 项两行(4+3),左右分栏展示 -->
+    <section class="panel overview-panel" aria-label="运营概览">
+      <div class="panel-top">
+        <h2 class="title-bar-heading">数据概览</h2>
+      </div>
+      <div class="overview-panel__body">
+        <div class="stats stats--overview">
+          <div
+            v-for="card in overviewCards"
+            :key="card.key"
+            class="stat stat--overview"
+            :class="{ warn: card.warn, 'stat--risk': card.risk }"
+            role="button"
+            tabindex="0"
+            @click="handleOverviewClick(card.key)"
+            @keydown.enter="handleOverviewClick(card.key)"
+          >
+            <div class="stat-overview-left">
+              <span class="stat-ic stat-ic--compact" aria-hidden="true">
+                <el-icon :size="22"><component :is="card.icon" /></el-icon>
+              </span>
+              <div class="stat-overview-titles">
+                <div class="k">{{ card.title }}</div>
+              </div>
+            </div>
+            <div class="stat-overview-right">
+              <div class="v">{{ card.value }}</div>
+              <div class="hint todo-hint-block stat-overview-hint">
+                <div v-for="(line, idx) in card.hintLines" :key="idx" class="todo-hint-line">{{ line }}</div>
+              </div>
+            </div>
           </div>
         </div>
-      </el-col>
-    </el-row>
-
-    <!-- 待办事项 -->
-    <el-row :gutter="16" class="todo-row">
-      <el-col :xs="24" :sm="12" :md="6" v-for="todo in todoList" :key="todo.type">
-        <div class="todo-card" :class="todo.className" @click="handleTodoClick(todo.path)">
-          <div class="todo-header">
-            <span class="todo-title">{{ todo.title }}</span>
-            <span class="todo-count" :class="todo.countClass">{{ todo.count }}</span>
+      </div>
+    </section>
+
+    <!-- 近30日:每日营业额、每日订单数(数据来自 statistics/orders/trend) -->
+    <section class="panel trend-charts-panel" aria-label="近30日趋势">
+      <div class="panel-top">
+        <h2 class="title-bar-heading">近30日趋势</h2>
+      </div>
+      <div class="trend-charts-panel__body">
+        <div class="trend-charts-grid">
+          <div class="trend-chart-card">
+            <div class="trend-chart-card__head">
+              <h3 class="trend-chart-card__title">每日营业额</h3>
+              <span class="trend-chart-card__unit">单位:元</span>
+            </div>
+            <ArtLineChart
+              class="trend-chart-card__chart"
+              height="240px"
+              :loading="trendLoading"
+              :data="revenueSeries"
+              :xAxisData="trendXAxis"
+              :colors="consoleChartColors.trendLineRevenue"
+              :showAreaColor="true"
+              :showAxisLine="false"
+            />
           </div>
-          <div class="todo-list">
-            <div v-for="(item, index) in todo.items.slice(0, 2)" :key="index" class="todo-item">
-              <span class="todo-item-text">{{ item }}</span>
-              <span class="todo-item-action" @click.stop="handleTodoAction(todo.path)">
-                {{ todo.actionText }}
-              </span>
+          <div class="trend-chart-card">
+            <div class="trend-chart-card__head">
+              <h3 class="trend-chart-card__title">每日订单总数</h3>
+              <span class="trend-chart-card__unit">单位:笔</span>
             </div>
+            <ArtLineChart
+              class="trend-chart-card__chart"
+              height="240px"
+              :loading="trendLoading"
+              :data="orderCountSeries"
+              :xAxisData="trendXAxis"
+              :colors="consoleChartColors.trendLineOrders"
+              :showAreaColor="true"
+              :showAxisLine="false"
+            />
           </div>
         </div>
-      </el-col>
-    </el-row>
-
-    <!-- 快捷操作 -->
-    <el-row :gutter="16" class="quick-row">
-      <el-col :xs="12" :sm="6" v-for="action in quickActions" :key="action.key">
-        <div class="quick-btn" @click="handleQuickAction(action.path)">
-          <div class="quick-icon" :class="action.colorClass">
-            <el-icon :size="20"><component :is="action.icon" /></el-icon>
+      </div>
+    </section>
+
+    <!-- 今日各区域 + 今日下单客户:分组双柱(双 Y 轴) -->
+    <section class="panel region-today-panel" aria-label="今日各区域与客户统计">
+      <div class="panel-top">
+        <h2 class="title-bar-heading">今日各区域</h2>
+        <span class="region-today-panel__hint">
+          按当日创建订单统计;左侧为今日各区域营业额(元)与订单数(笔);右侧为今日下单客户的下单金额(元)与商品数量(件);类目较多时可拖动底部滑块或按住图表左右平移查看
+        </span>
+      </div>
+      <div class="region-today-panel__body">
+        <div class="region-today-split">
+          <div class="region-today-split__col">
+            <h3 class="region-today-subtitle">今日各区域</h3>
+            <RegionTodayDualBarChart
+              height="340px"
+              :loading="regionTodayLoading"
+              :categories="regionTodayCategories"
+              :amounts="regionTodayAmounts"
+              :counts="regionTodayCounts"
+              :amount-color="consoleChartColors.regionAmount"
+              :count-color="consoleChartColors.regionCount"
+            />
+          </div>
+          <div class="region-today-split__col">
+            <h3 class="region-today-subtitle">今日下单客户</h3>
+            <RegionTodayDualBarChart
+              height="340px"
+              :loading="clientTodayLoading"
+              :categories="clientTodayCategories"
+              :amounts="clientTodayAmounts"
+              :counts="clientTodayQuantities"
+              empty-text="暂无今日下单客户"
+              amount-series-name="下单金额(元)"
+              count-series-name="商品数量"
+              left-axis-name="元"
+              right-axis-name="数量"
+              count-value-suffix="件"
+              :amount-color="consoleChartColors.clientAmount"
+              :count-color="consoleChartColors.clientCount"
+            />
           </div>
-          <span class="quick-text">{{ action.text }}</span>
-          <el-icon class="quick-arrow"><ArrowRight /></el-icon>
         </div>
-      </el-col>
-    </el-row>
+      </div>
+    </section>
 
-    <!-- 最新订单 -->
+    <!-- 最新订单:与订单列表页一致 list-body + list-table + stripe,无 border -->
     <section class="panel orders-card">
       <div class="panel-top">
-        <h2>最新订单</h2>
-        <el-button type="primary" link @click="goToOrderList">查看更多</el-button>
+        <h2 class="title-bar-heading">最新订单</h2>
+        <el-button type="primary" link class="console-more-btn" @click="goToOrderList">
+          <span class="console-more-btn__inner">
+            查看更多
+            <el-icon class="console-more-btn__arrow" :size="15" aria-hidden="true"><ArrowRight /></el-icon>
+          </span>
+        </el-button>
       </div>
       <div class="list-body list-wrap">
-        <el-table class="list-table" :data="orderList" v-loading="loading" stripe>
-        <el-table-column prop="orderNo" label="订单编号" min-width="180" />
-        <el-table-column prop="clientName" label="客户名称" min-width="150" />
-        <el-table-column prop="region" label="区域" min-width="100" />
-        <el-table-column prop="totalAmount" label="订单金额(元)" min-width="120">
-          <template #default="{ row }">
-            <span class="amount-text">¥{{ row.totalAmount }}</span>
-          </template>
-        </el-table-column>
-        <el-table-column prop="status" label="订单状态" min-width="130">
-          <template #default="{ row }">
-            <el-tag :type="getStatusType(row.status)" size="small">
-              {{ getStatusText(row.status) }}
-            </el-tag>
-          </template>
-        </el-table-column>
-        <el-table-column prop="createTime" label="下单时间" min-width="180" />
-        <el-table-column label="操作" min-width="100">
-          <template #default="{ row }">
-            <el-button type="primary" link @click="goToOrderDetail(row.id)">查看</el-button>
-          </template>
-        </el-table-column>
+        <el-table
+          class="order-list-table list-table"
+          :data="orderList"
+          v-loading="loading"
+          stripe
+          row-key="id"
+        >
+          <el-table-column prop="orderNo" label="订单编号" min-width="180" show-overflow-tooltip />
+          <el-table-column prop="clientName" label="客户名称" min-width="150" show-overflow-tooltip />
+          <el-table-column prop="regionName" label="区域" min-width="100" show-overflow-tooltip />
+          <el-table-column prop="totalAmount" label="订单金额(元)" min-width="120" align="right">
+            <template #default="{ row }">
+              <span class="amount-text">¥{{ formatMoney(row.totalAmount) }}</span>
+            </template>
+          </el-table-column>
+          <el-table-column label="订单状态" min-width="130">
+            <template #default="{ row }">
+              <el-tag :type="getOrderStatusType(resolveOrderStatus(row))" size="small">
+                {{ getOrderStatusText(resolveOrderStatus(row)) }}
+              </el-tag>
+            </template>
+          </el-table-column>
+          <el-table-column prop="createAt" label="下单时间" min-width="180">
+            <template #default="{ row }">
+              {{ formatDateTime(row.createAt) }}
+            </template>
+          </el-table-column>
+          <el-table-column label="操作" min-width="100" fixed="right">
+            <template #default="{ row }">
+              <el-button class="btn-op-item" @click.stop="goToOrderDetail(row)">
+                <el-icon><View /></el-icon>
+                查看
+              </el-button>
+            </template>
+          </el-table-column>
         </el-table>
       </div>
     </section>
@@ -97,474 +192,619 @@
 </template>
 
 <script setup lang="ts">
-  import { ref, onMounted } from 'vue'
+  import { ref, computed, onMounted } from 'vue'
   import { useRouter } from 'vue-router'
   import {
     WarningFilled,
     UserFilled,
-    User,
     Shop,
     ShoppingCart,
     Van,
     Money,
-    Plus,
-    Wallet,
+    Document,
+    View,
     ArrowRight
   } from '@element-plus/icons-vue'
-  import { ElMessage } from 'element-plus'
+  import {
+    fetchWorkbenchDashboard,
+    fetchOrderTrend,
+    fetchTodayOrdersByRegion,
+    fetchTodayOrdersByClient,
+    type WorkbenchDashboardData,
+    type OrderTrendDayItem,
+    type TodayRegionOrderStatItem,
+    type TodayClientOrderStatItem
+  } from '@/api/business'
+  import RegionTodayDualBarChart from './RegionTodayDualBarChart.vue'
 
   defineOptions({ name: 'Console' })
 
+  /** 工作台四图配色:两条折线 + 两组双柱共 6 色互不重复,便于区分 */
+  const consoleChartColors = {
+    trendLineRevenue: ['#10b981'],
+    /** 每日订单总数折线:金色 */
+    trendLineOrders: ['#d4a017'],
+    regionAmount: '#0d9488',
+    regionCount: '#d97706',
+    /** 今日下单客户:绿柱 + 金柱 */
+    clientAmount: '#10b981',
+    clientCount: '#d97706'
+  } as const
+
   const router = useRouter()
   const loading = ref(false)
-  const warningCount = ref(3)
-
-  // 统计数据
-  const statsData = ref([
-    { key: 'client', label: '客户总数', value: '1,286', icon: UserFilled, colorClass: 'blue', path: '/admin/user' },
-    { key: 'supplier', label: '供应商数量', value: '86', icon: Shop, colorClass: 'green', path: '/admin/user' },
-    { key: 'todayOrder', label: '今日订单', value: '328', icon: ShoppingCart, colorClass: 'orange', path: '/admin/order' },
-    { key: 'pendingDelivery', label: '待配送', value: '45', icon: Van, colorClass: 'red', path: '/admin/delivery' },
-    { key: 'todayAmount', label: '今日交易额', value: '¥58,620', icon: Money, colorClass: 'purple', path: '/admin/statistics' }
-  ])
-
-  // 待办事项
-  const todoList = ref([
-    {
-      type: 'quotation',
-      title: '待审核报价',
-      count: 5,
-      className: 'warning',
-      countClass: 'orange',
-      actionText: '处理',
-      path: '/inquiry/quotation-list',
-      items: ['2026年3月第3周询价...', '城东区域供应商报价...']
-    },
-    {
-      type: 'delivery',
-      title: '待分配配送',
-      count: 12,
-      className: 'danger',
-      countClass: '',
-      actionText: '分配',
-      path: '/admin/delivery',
-      items: ['ORD202603190003', 'ORD202603190004']
-    },
-    {
-      type: 'cancel',
-      title: '取消待审核',
-      count: 3,
-      className: 'primary',
-      countClass: 'blue',
-      actionText: '审核',
-      path: '/admin/order',
-      items: ['客户BB超市申请取消...', '客户CC商行申请取消...']
-    },
-    {
-      type: 'settlement',
-      title: '待结算确认',
-      count: 8,
-      className: 'success',
-      countClass: 'green',
-      actionText: '确认',
-      path: '/admin/settlement',
-      items: ['AA餐厅线下结算...', 'BB超市线下结算...']
-    }
-  ])
-
-  // 快捷操作
-  const quickActions = [
-    { key: 'inquiry', text: '发布询价', icon: Plus, colorClass: 'blue', path: '/inquiry/inquiry-list' },
-    { key: 'delivery', text: '分配配送', icon: Van, colorClass: 'orange', path: '/admin/delivery' },
-    { key: 'settlement', text: '确认结算', icon: Wallet, colorClass: 'green', path: '/admin/settlement' },
-    { key: 'user', text: '新增用户', icon: User, colorClass: 'red', path: '/admin/user' }
-  ]
-
-  // 订单列表
-  const orderList = ref([
-    {
-      id: '1',
-      orderNo: 'ORD202603190001',
-      clientName: 'XX餐厅',
-      region: '城东',
-      totalAmount: '1,286.00',
-      status: 'pending_payment',
-      createTime: '2026-03-19 10:23:45'
-    },
-    {
-      id: '2',
-      orderNo: 'ORD202603190002',
-      clientName: 'YY超市',
-      region: '城西',
-      totalAmount: '2,560.00',
-      status: 'pending_delivery',
-      createTime: '2026-03-19 09:15:22'
-    },
-    {
-      id: '3',
-      orderNo: 'ORD202603180056',
-      clientName: 'ZZ食堂',
-      region: '城南',
-      totalAmount: '856.00',
-      status: 'delivering',
-      createTime: '2026-03-18 16:45:30'
-    },
-    {
-      id: '4',
-      orderNo: 'ORD202603180055',
-      clientName: 'AA饭店',
-      region: '城东',
-      totalAmount: '3,200.00',
-      status: 'completed',
-      createTime: '2026-03-18 14:20:18'
-    }
-  ])
+  const dashboard = ref<WorkbenchDashboardData | null>(null)
+
+  const TREND_DAYS = 30
+  const trendLoading = ref(false)
+  const orderTrend = ref<OrderTrendDayItem[]>([])
+
+  const trendXAxis = computed(() =>
+    orderTrend.value.map((row) => {
+      const d = row.date || ''
+      return d.length >= 10 ? `${d.slice(5, 7)}-${d.slice(8, 10)}` : d
+    })
+  )
+
+  const revenueSeries = computed(() => orderTrend.value.map((r) => Number(r.orderAmount ?? 0)))
+  const orderCountSeries = computed(() => orderTrend.value.map((r) => Number(r.orderCount ?? 0)))
+
+  const regionTodayLoading = ref(false)
+  const regionTodayList = ref<TodayRegionOrderStatItem[]>([])
+
+  const regionTodayCategories = computed(() => regionTodayList.value.map((r) => r.regionName || r.regionId))
+  const regionTodayAmounts = computed(() => regionTodayList.value.map((r) => Number(r.orderAmount ?? 0)))
+  const regionTodayCounts = computed(() => regionTodayList.value.map((r) => Number(r.orderCount ?? 0)))
+
+  const clientTodayLoading = ref(false)
+  const clientTodayList = ref<TodayClientOrderStatItem[]>([])
+
+  const clientTodayCategories = computed(() =>
+    clientTodayList.value.map((r) => {
+      const n = String(r.clientName || r.clientId || '')
+      return n.length > 10 ? `${n.slice(0, 10)}…` : n
+    })
+  )
+  const clientTodayAmounts = computed(() => clientTodayList.value.map((r) => Number(r.orderAmount ?? 0)))
+  const clientTodayQuantities = computed(() => clientTodayList.value.map((r) => Number(r.productQuantity ?? 0)))
+
+  const warningCount = computed(() => Math.max(0, Number(dashboard.value?.deliveryWarningCount ?? 0)))
+
+  const formatMoney = (n: number | string | undefined) => {
+    const num = typeof n === 'string' ? parseFloat(n) : Number(n ?? 0)
+    if (Number.isNaN(num)) return '0.00'
+    return num.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
+  }
+
+  const formatDateTime = (t: string | undefined | null) => {
+    if (t == null || t === '') return '—'
+    return String(t).replace('T', ' ').slice(0, 19)
+  }
+
+  const overviewCards = computed(() => {
+    const d = dashboard.value
+    const c = d?.customerCount ?? 0
+    const s = d?.supplierCount ?? 0
+    const t = d?.todayOrderCount ?? 0
+    const p = d?.pendingDeliveryCount ?? 0
+    const a = d?.todayOrderAmount ?? 0
+    const snaps = d?.todoSnapshots || []
+    const quotSnap = snaps.find((x) => x.type === 'quotation')
+    const qCount = quotSnap?.count ?? 0
+    const quotLines =
+      loading.value && !d
+        ? ['加载中…']
+        : quotSnap?.items?.length
+          ? quotSnap.items.slice(0, 2)
+          : ['暂无新报价待处理']
+
+    return [
+      {
+        key: 'client',
+        title: '客户总数',
+        value: c.toLocaleString('zh-CN'),
+        hintLines: ['有效客户账号'],
+        icon: UserFilled,
+        warn: false,
+        risk: false
+      },
+      {
+        key: 'supplier',
+        title: '供应商数量',
+        value: s.toLocaleString('zh-CN'),
+        hintLines: ['有效供应商账号'],
+        icon: Shop,
+        warn: false,
+        risk: false
+      },
+      {
+        key: 'todayOrder',
+        title: '今日订单',
+        value: t.toLocaleString('zh-CN'),
+        hintLines: [d?.todayOrderCountHint || '—'],
+        icon: ShoppingCart,
+        warn: false,
+        risk: false
+      },
+      {
+        key: 'pendingDelivery',
+        title: '待配送',
+        value: p.toLocaleString('zh-CN'),
+        hintLines: [p > 0 ? '含待分配配送的订单' : '暂无待配送'],
+        icon: Van,
+        warn: p > 0,
+        risk: false
+      },
+      {
+        key: 'todayAmount',
+        title: '今日交易额',
+        value: `¥${formatMoney(a)}`,
+        hintLines: [d?.todayOrderAmountHint || '—'],
+        icon: Money,
+        warn: false,
+        risk: false
+      },
+      {
+        key: 'newQuotation',
+        title: '新的报价',
+        value: qCount.toLocaleString('zh-CN'),
+        hintLines: quotLines,
+        icon: Document,
+        warn: qCount > 0,
+        risk: false
+      }
+    ]
+  })
+
+  const orderList = computed(() => dashboard.value?.recentOrders ?? [])
+
+  const resolveOrderStatus = (row: { cancelStatus?: string; orderStatus?: string; status?: string }) => {
+    if (row?.cancelStatus === 'pending_review') return 'pending_review_cancel'
+    if (row?.orderStatus) return row.orderStatus
+    if (row?.status === 'pending_payment') return 'pending_payment'
+    if (['completed'].includes(String(row?.status))) return 'completed'
+    if (['cancelled'].includes(String(row?.status))) return 'cancelled'
+    return 'pending_confirm'
+  }
 
-  // 获取状态类型
-  const getStatusType = (status: string) => {
-    const typeMap: Record<string, string> = {
+  const getOrderStatusType = (status: string) => {
+    const map: Record<string, string> = {
       pending_payment: 'warning',
-      pending_delivery: 'primary',
-      delivering: 'info',
-      partial_delivered: 'warning',
       pending_confirm: 'primary',
+      pending: 'primary',
+      pending_review_cancel: 'warning',
       completed: 'success',
       cancelled: 'info'
     }
-    return typeMap[status] || 'info'
+    return map[status] || 'info'
   }
 
-  // 获取状态文本
-  const getStatusText = (status: string) => {
-    const textMap: Record<string, string> = {
+  const getOrderStatusText = (status: string) => {
+    const map: Record<string, string> = {
       pending_payment: '待支付',
-      pending_delivery: '待分配配送',
-      delivering: '配送中',
-      partial_delivered: '部分送达',
-      pending_confirm: '待确认收货',
+      pending_confirm: '待收货',
+      pending: '待收货',
+      pending_review_cancel: '待审核取消',
       completed: '已完成',
       cancelled: '已取消'
     }
-    return textMap[status] || status
+    return map[status] || status
   }
 
-  // 跳转预警页面
   const goToWarning = () => {
-    router.push('/admin/delivery-warning')
+    router.push('/delivery/delivery-warning')
   }
 
-  // 统计卡片点击
-  const handleStatClick = (key: string) => {
-    const stat = statsData.value.find((s) => s.key === key)
-    if (stat?.path) {
-      router.push(stat.path)
+  const handleOverviewClick = (key: string) => {
+    const map: Record<string, string> = {
+      client: '/base/user',
+      supplier: '/base/user',
+      todayOrder: '/order/order',
+      pendingDelivery: '/delivery/delivery',
+      todayAmount: '/order/order',
+      newQuotation: '/inquiry/manage'
     }
+    const path = map[key]
+    if (path) router.push(path)
   }
 
-  // 待办点击
-  const handleTodoClick = (path: string) => {
-    router.push(path)
+  const goToOrderList = () => {
+    router.push('/order/order')
   }
 
-  // 待办操作
-  const handleTodoAction = (path: string) => {
-    router.push(path)
+  const goToOrderDetail = (row: { id: string }) => {
+    router.push({ path: '/order/order', query: { viewOrderId: row.id } })
   }
 
-  // 快捷操作
-  const handleQuickAction = (path: string) => {
-    router.push(path)
+  const loadDashboard = async () => {
+    loading.value = true
+    try {
+      dashboard.value = await fetchWorkbenchDashboard()
+    } catch {
+      dashboard.value = null
+    } finally {
+      loading.value = false
+    }
   }
 
-  // 跳转订单列表
-  const goToOrderList = () => {
-    router.push('/admin/order')
+  const loadOrderTrend = async () => {
+    trendLoading.value = true
+    try {
+      const list = await fetchOrderTrend({ days: TREND_DAYS })
+      orderTrend.value = Array.isArray(list) ? list : []
+    } catch {
+      orderTrend.value = []
+    } finally {
+      trendLoading.value = false
+    }
+  }
+
+  const loadTodayRegionStats = async () => {
+    regionTodayLoading.value = true
+    try {
+      const list = await fetchTodayOrdersByRegion()
+      regionTodayList.value = Array.isArray(list) ? list : []
+    } catch {
+      regionTodayList.value = []
+    } finally {
+      regionTodayLoading.value = false
+    }
   }
 
-  // 跳转订单详情
-  const goToOrderDetail = (id: string) => {
-    router.push(`/admin/order/${id}`)
+  const loadTodayClientStats = async () => {
+    clientTodayLoading.value = true
+    try {
+      const list = await fetchTodayOrdersByClient({ limit: 30 })
+      clientTodayList.value = Array.isArray(list) ? list : []
+    } catch {
+      clientTodayList.value = []
+    } finally {
+      clientTodayLoading.value = false
+    }
   }
 
   onMounted(() => {
-    // TODO: 从后端获取数据
+    void loadDashboard()
+    void loadOrderTrend()
+    void loadTodayRegionStats()
+    void loadTodayClientStats()
   })
 </script>
 
 <style lang="scss" scoped>
-.console-container {
-  .alert-bar {
-    display: flex;
-    align-items: center;
-    gap: 12px;
-    padding: 12px 16px;
-    margin-bottom: 16px;
-    background: #fffbeb;
-    border: 1px solid #fde68a;
-    border-radius: var(--mint-radius);
-    cursor: pointer;
-    transition: 0.2s ease;
+  .console-container {
+    .alert-bar {
+      display: flex;
+      align-items: center;
+      gap: 12px;
+      padding: 12px 16px;
+      margin-bottom: 16px;
+      background: #fffbeb;
+      border: 1px solid #fde68a;
+      border-radius: var(--mint-radius);
+      cursor: pointer;
+      transition: 0.2s ease;
 
-    &:hover {
-      background: #fef3c7;
-    }
+      &:hover {
+        background: #fef3c7;
+      }
 
-    .alert-icon {
-      color: #d97706;
-      font-size: 18px;
-    }
+      .alert-icon {
+        color: #d97706;
+        font-size: 18px;
+      }
 
-    .alert-text {
-      flex: 1;
-      color: #92400e;
-      font-size: 13px;
-    }
+      .alert-text {
+        flex: 1;
+        color: #92400e;
+        font-size: 13px;
+      }
 
-    .alert-btn {
-      color: var(--mint-primary-dark);
-      font-size: 13px;
-      font-weight: 600;
+      .alert-btn {
+        color: var(--mint-primary-dark);
+        font-size: 13px;
+        font-weight: 600;
+      }
     }
   }
-}
-
-.stats-row,
-.todo-row,
-.quick-row {
-  margin-bottom: 16px;
-}
-
-.stats-card,
-.todo-card,
-.quick-btn {
-  background: var(--mint-surface);
-  border: 1px solid var(--mint-border);
-  border-radius: var(--mint-radius);
-  box-shadow: var(--mint-shadow);
-  margin-bottom: 0;
-}
-
-.stats-card {
-  min-height: 112px;
-  padding: 12px 14px;
-  gap: 12px;
-  transition: border-color 0.2s ease;
-
-  &:hover {
-    border-color: #b9dbcb;
-    transform: none;
-    box-shadow: var(--mint-shadow);
-  }
-
-  .stats-icon {
-    width: 44px;
-    height: 44px;
-    border-radius: 10px;
-    border: 1px solid rgba(16, 185, 129, 0.12);
-    background: var(--mint-primary-bg);
-    color: var(--mint-primary-dark);
 
-    &.orange,
-    &.red {
-      background: #fffbeb;
-      border-color: rgba(217, 119, 6, 0.2);
-      color: #d97706;
-    }
+  .overview-panel,
+  .trend-charts-panel,
+  .region-today-panel {
+    margin-bottom: 16px;
+  }
 
-    &.blue,
-    &.green,
-    &.purple {
-      background: var(--mint-primary-bg);
-      border-color: rgba(16, 185, 129, 0.12);
-      color: var(--mint-primary-dark);
-    }
+  .overview-panel__body {
+    padding: 16px;
   }
 
-  .stats-info {
-    .stats-num {
-      font-size: 22px;
-      font-weight: 700;
-      color: var(--mint-text);
-      line-height: 1.2;
-      letter-spacing: -0.02em;
-      font-variant-numeric: tabular-nums;
-    }
+  .trend-charts-panel__body {
+    padding: 16px;
+    border-top: 1px solid var(--mint-border, #d9e8e2);
+  }
+
+  .trend-charts-grid {
+    display: grid;
+    grid-template-columns: repeat(2, minmax(0, 1fr));
+    gap: 16px;
+  }
 
-    .stats-label {
-      font-size: 12px;
-      color: var(--mint-text-muted);
-      margin-top: 4px;
+  @media (max-width: 960px) {
+    .trend-charts-grid {
+      grid-template-columns: 1fr;
     }
   }
-}
 
-.todo-card {
-  padding: 14px;
-  border-left: 3px solid var(--mint-border);
-  cursor: pointer;
-  transition: 0.2s ease;
+  .trend-chart-card {
+    min-width: 0;
+    padding: 14px 14px 8px;
+    background: var(--mint-surface, #fff);
+    border: 1px solid var(--mint-border, #d9e8e2);
+    border-radius: var(--mint-radius, 10px);
+    box-shadow: var(--mint-shadow, 0 1px 3px rgba(0, 0, 0, 0.05));
+  }
 
-  &:hover {
-    border-color: var(--mint-primary);
-    transform: none;
-    box-shadow: var(--mint-shadow);
+  .trend-chart-card__head {
+    display: flex;
+    align-items: baseline;
+    justify-content: space-between;
+    gap: 12px;
+    margin-bottom: 8px;
   }
 
-  &.warning {
-    border-left-color: #d97706;
+  .trend-chart-card__title {
+    margin: 0;
+    font-size: 14px;
+    font-weight: 600;
+    color: var(--mint-text, #1e293b);
   }
 
-  &.danger {
-    border-left-color: #dc2626;
+  .trend-chart-card__unit {
+    font-size: 11px;
+    font-weight: 500;
+    color: var(--mint-text-muted, #64748b);
+    flex-shrink: 0;
   }
 
-  &.primary {
-    border-left-color: var(--mint-primary-dark);
+  .trend-chart-card__chart {
+    margin-left: -6px;
   }
 
-  &.success {
-    border-left-color: var(--mint-primary);
+  .region-today-panel .panel-top {
+    flex-wrap: wrap;
+    align-items: flex-start;
+    gap: 8px;
   }
 
-  .todo-header {
-    display: flex;
-    justify-content: space-between;
-    align-items: center;
-    margin-bottom: 12px;
+  .region-today-panel__hint {
+    font-size: 12px;
+    font-weight: 500;
+    color: var(--mint-text-muted, #64748b);
+    line-height: 1.4;
+    flex: 1 1 220px;
+    min-width: 0;
+  }
 
-    .todo-title {
-      font-size: 14px;
-      font-weight: 600;
-      color: var(--mint-text);
-    }
+  .region-today-panel__body {
+    padding: 16px;
+    border-top: 1px solid var(--mint-border, #d9e8e2);
+  }
 
-    .todo-count {
-      background: var(--mint-primary-bg);
-      color: var(--mint-primary-dark);
-      padding: 2px 10px;
-      border-radius: 6px;
-      font-size: 12px;
-      font-weight: 600;
+  .region-today-split {
+    display: grid;
+    grid-template-columns: repeat(2, minmax(0, 1fr));
+    gap: 16px;
+  }
 
-      &.blue {
-        background: var(--mint-primary-bg);
-        color: var(--mint-primary-dark);
-      }
+  @media (max-width: 1100px) {
+    .region-today-split {
+      grid-template-columns: 1fr;
+    }
+  }
 
-      &.orange {
-        background: #fffbeb;
-        color: #d97706;
-      }
+  .region-today-split__col {
+    min-width: 0;
+  }
 
-      &.green {
-        background: #d1fae5;
-        color: #047857;
-      }
+  .region-today-subtitle {
+    margin: 0 0 10px;
+    font-size: 13px;
+    font-weight: 600;
+    color: var(--mint-text, #1e293b);
+    display: inline-flex;
+    align-items: center;
+    gap: 8px;
+
+    &::before {
+      content: '';
+      width: 3px;
+      height: 14px;
+      border-radius: 2px;
+      background: var(--mint-primary, #10b981);
+      flex-shrink: 0;
     }
   }
 
-  .todo-list {
-    .todo-item {
-      display: flex;
-      justify-content: space-between;
-      align-items: center;
-      padding: 8px 0;
-      border-bottom: 1px solid #edf4f1;
-      font-size: 13px;
+  /* 与 delivery-demo `.stats` / `.stat` 对齐(色值与 demo 一致) */
+  .stats {
+    display: grid;
+    gap: 12px;
+    margin-bottom: 0;
+  }
 
-      &:last-child {
-        border-bottom: none;
-      }
+  .stats--overview {
+    grid-template-columns: repeat(4, minmax(0, 1fr));
+  }
 
-      .todo-item-text {
-        color: var(--mint-text-muted);
-        flex: 1;
-        overflow: hidden;
-        text-overflow: ellipsis;
-        white-space: nowrap;
-      }
+  @media (max-width: 1200px) {
+    .stats--overview {
+      grid-template-columns: repeat(2, minmax(0, 1fr));
+    }
+  }
 
-      .todo-item-action {
-        color: var(--mint-primary-dark);
-        cursor: pointer;
-        margin-left: 8px;
+  @media (max-width: 640px) {
+    .stats--overview {
+      grid-template-columns: 1fr;
+    }
+  }
 
-        &:hover {
-          text-decoration: underline;
-        }
-      }
+  .stat--overview {
+    background: var(--mint-surface, #fff);
+    border: 1px solid var(--mint-border, #d9e8e2);
+    border-radius: 10px;
+    padding: 10px 14px;
+    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
+    display: flex;
+    flex-direction: row;
+    align-items: center;
+    justify-content: space-between;
+    gap: 12px;
+    min-height: 0;
+    cursor: pointer;
+    transition: border-color 0.2s ease;
+
+    &:hover {
+      border-color: #b9dbcb;
     }
   }
-}
 
-.quick-btn {
-  border-radius: var(--mint-radius);
-  padding: 16px;
-  display: flex;
-  align-items: center;
-  gap: 12px;
-  cursor: pointer;
-  transition: border-color 0.2s ease;
+  .stat-overview-left {
+    display: flex;
+    align-items: center;
+    gap: 10px;
+    min-width: 0;
+    flex: 1;
+  }
 
-  &:hover {
-    border-color: #b9dbcb;
-    transform: none;
-    box-shadow: var(--mint-shadow);
+  .stat-overview-titles .k {
+    margin: 0;
   }
 
-  .quick-icon {
-    width: 40px;
-    height: 40px;
+  .stat-overview-right {
+    flex-shrink: 0;
+    text-align: right;
+    max-width: 52%;
+    min-width: 0;
+  }
+
+  .stat-overview-hint {
+    text-align: right;
+  }
+
+  .stat-overview-hint .todo-hint-line {
+    text-align: right;
+  }
+
+  .stat-ic {
+    width: 44px;
+    height: 44px;
     border-radius: 10px;
     display: flex;
     align-items: center;
     justify-content: center;
+    background: #ecfdf5;
+    color: #059669;
+    border: 1px solid rgba(16, 185, 129, 0.12);
+    flex-shrink: 0;
+  }
 
-    &.blue {
-      background: var(--mint-primary-bg);
-      color: var(--mint-primary-dark);
-    }
+  .stat-ic--compact {
+    width: 40px;
+    height: 40px;
+    border-radius: 10px;
+  }
 
-    &.orange {
-      background: #fffbeb;
-      color: #d97706;
-    }
+  .stat .k {
+    font-size: 13px;
+    color: var(--mint-text, #1e293b);
+    font-weight: 600;
+    line-height: 1.35;
+  }
 
-    &.green {
-      background: #d1fae5;
-      color: #047857;
-    }
+  .stat .v {
+    font-size: 20px;
+    font-weight: 700;
+    line-height: 1.15;
+    color: var(--mint-text, #1e293b);
+    font-variant-numeric: tabular-nums;
+  }
 
-    &.red {
-      background: #fef2f2;
-      color: #dc2626;
-    }
+  .stat .hint {
+    margin-top: 2px;
+    font-size: 11px;
+    color: #475569;
   }
 
-  .quick-text {
-    font-size: 14px;
-    font-weight: 500;
-    color: var(--mint-text);
-    flex: 1;
+  .todo-hint-block {
+    white-space: normal;
   }
 
-  .quick-arrow {
-    color: var(--mint-text-muted);
+  .todo-hint-line {
+    font-size: 11px;
+    color: #64748b;
+    line-height: 1.45;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    display: -webkit-box;
+    -webkit-line-clamp: 2;
+    -webkit-box-orient: vertical;
   }
-}
 
-.orders-card {
-  .list-body {
-    border-top: 1px solid var(--mint-border);
+  .stat.warn .stat-ic {
+    background: #fffbeb;
+    color: #d97706;
+    border-color: rgba(217, 119, 6, 0.2);
   }
 
-  :deep(.el-table__cell .cell) {
-    white-space: nowrap;
+  .stat.warn .v,
+  .stat.warn .hint {
+    color: #d97706;
   }
 
-  .amount-text {
-    color: var(--mint-primary-dark);
-    font-weight: 600;
+  .stat.warn .todo-hint-line {
+    color: #b45309;
+  }
+
+  .stat--risk .stat-ic {
+    background: #fef2f2;
+    color: #dc2626;
+    border-color: rgba(220, 38, 38, 0.2);
+  }
+
+  .stat--risk .v {
+    color: #dc2626;
+  }
+
+  .console-more-btn__inner {
+    display: inline-flex;
+    align-items: center;
+    gap: 4px;
+  }
+
+  .console-more-btn__arrow {
+    flex-shrink: 0;
+  }
+
+  .orders-card {
+    .list-body {
+      border-top: 1px solid var(--mint-border);
+    }
+
+    :deep(.el-table__cell .cell) {
+      white-space: nowrap;
+    }
+
+    .amount-text {
+      color: var(--mint-primary-dark);
+      font-weight: 600;
+      font-variant-numeric: tabular-nums;
+    }
+  }
+
+  /* 与订单列表页一致:单元格 flex 垂直居中,避免状态列错位 */
+  .order-list-table :deep(th.el-table__cell > .cell),
+  .order-list-table :deep(td.el-table__cell > .cell) {
+    display: flex;
+    align-items: center;
   }
-}
 </style>

+ 52 - 34
web-admin/src/views/system/inquiry-quotation/index.vue

@@ -154,7 +154,13 @@
                   <template v-if="selectedQuotationDetail">
                     <div class="selected-quote-view-hero">
                       <div class="selected-quote-view-hero-icon">
-                        <el-icon><Document /></el-icon>
+                        <img
+                          v-if="selectedSupplierLogoDisplayUrl"
+                          :src="selectedSupplierLogoDisplayUrl"
+                          alt=""
+                          class="selected-quote-view-hero-logo"
+                        />
+                        <el-icon v-else><Document /></el-icon>
                       </div>
                       <div class="selected-quote-view-hero-main">
                         <h2 :class="supplierQuoteToneClass(selectedSupplierQuoteTone)">
@@ -166,32 +172,28 @@
                         <p class="selected-quote-view-hero-meta">
                           报价单创建时间:{{ selectedQuotationDetail.quotationCreateAt || '-' }}
                         </p>
+                        <dl class="view-kv-grid selected-quote-view-hero-kv">
+                          <div class="view-kv">
+                            <dt>供应商</dt>
+                            <dd :class="supplierQuoteToneClass(selectedSupplierQuoteTone)">
+                              {{ selectedQuotationDetail.supplierName || '-' }}
+                            </dd>
+                          </div>
+                          <div class="view-kv">
+                            <dt>报价单ID</dt>
+                            <dd>{{ selectedQuotationDetail.quotationId || '-' }}</dd>
+                          </div>
+                          <div class="view-kv">
+                            <dt>价格清单ID</dt>
+                            <dd>{{ selectedQuotationDetail.priceListId || '-' }}</dd>
+                          </div>
+                          <div class="view-kv">
+                            <dt>价格版本</dt>
+                            <dd>V{{ selectedQuotationDetail.priceVersion || '-' }}</dd>
+                          </div>
+                        </dl>
                       </div>
                     </div>
-
-                    <div class="view-section selected-quote-view-section">
-                      <h3>基础信息</h3>
-                      <dl class="view-kv-grid">
-                        <div class="view-kv">
-                          <dt>供应商</dt>
-                          <dd :class="supplierQuoteToneClass(selectedSupplierQuoteTone)">
-                            {{ selectedQuotationDetail.supplierName || '-' }}
-                          </dd>
-                        </div>
-                        <div class="view-kv">
-                          <dt>报价单ID</dt>
-                          <dd>{{ selectedQuotationDetail.quotationId || '-' }}</dd>
-                        </div>
-                        <div class="view-kv">
-                          <dt>价格清单ID</dt>
-                          <dd>{{ selectedQuotationDetail.priceListId || '-' }}</dd>
-                        </div>
-                        <div class="view-kv">
-                          <dt>价格版本</dt>
-                          <dd>V{{ selectedQuotationDetail.priceVersion || '-' }}</dd>
-                        </div>
-                      </dl>
-                    </div>
                   </template>
                 </div>
 
@@ -1933,6 +1935,20 @@ const selectedSupplierQuoteTone = computed((): SupplierQuoteTone => {
   return 'full'
 })
 
+/** 已选中供应商 Logo(后端 sys_user.avatar_url → supplierLogoUrl) */
+const selectedSupplierLogoDisplayUrl = computed(() => {
+  const d = selectedQuotationDetail.value
+  if (!d) return ''
+  const raw =
+    d.supplierLogoUrl ??
+    d.supplier_logo_url ??
+    d.supplier_logo ??
+    d.supplierAvatarUrl ??
+    d.supplier_avatar_url ??
+    ''
+  return String(raw || '').trim()
+})
+
 const quotationDetailSupplierTone = computed((): SupplierQuoteTone => {
   const items = quotationDetailItems.value
   if (!items?.length) return 'partial'
@@ -2812,6 +2828,13 @@ onMounted(async () => {
   justify-content: center;
   font-size: 26px;
   flex: 0 0 auto;
+  overflow: hidden;
+}
+.selected-quote-view-hero-logo {
+  width: 100%;
+  height: 100%;
+  object-fit: cover;
+  display: block;
 }
 .selected-quote-view-hero-main {
   min-width: 0;
@@ -2833,15 +2856,10 @@ onMounted(async () => {
   font-size: 13px;
   color: var(--mint-text-muted, #64748b);
 }
-.selected-quote-view-section {
-  margin: 0;
-  padding: 14px 16px;
-  border: 0;
-  border-top: 1px solid var(--mint-border, #d9e8e2);
-  border-radius: 0;
-}
-.selected-quote-view-section h3 {
-  margin: 0 0 12px;
+.selected-quote-view-hero-kv {
+  margin-top: 12px;
+  padding-top: 12px;
+  border-top: 1px solid rgba(16, 185, 129, 0.15);
 }
 .selected-quote-view-note {
   margin-bottom: 0;

+ 22 - 0
web-admin/src/views/system/order/index.vue

@@ -1122,6 +1122,7 @@
     onUnmounted,
     watch
   } from 'vue'
+  import { useRoute, useRouter } from 'vue-router'
   import { ElMessage, ElMessageBox } from 'element-plus'
   import { withFullscreenLoading, fullscreenLoadingText } from '@/utils/fullscreen-loading'
   import {
@@ -1157,6 +1158,9 @@
 
   defineOptions({ name: 'OrderManagement' })
 
+  const route = useRoute()
+  const router = useRouter()
+
   const searchForm = reactive({
     keyword: '',
     orderStatus: '',
@@ -1820,6 +1824,20 @@
     }
   }
 
+  /** 工作台等入口:带 viewOrderId 进入时打开与「查看/运营」相同的详情弹窗 */
+  watch(
+    () => route.query.viewOrderId,
+    async (raw) => {
+      const id = typeof raw === 'string' ? raw.trim() : ''
+      if (!id) return
+      await handleView({ id })
+      const q = { ...route.query } as Record<string, string | string[] | undefined | null>
+      delete q.viewOrderId
+      await router.replace({ path: route.path, query: q })
+    },
+    { immediate: true }
+  )
+
   const handleAssign = (row: any) => {
     assignReassignMode.value = false
     assignForm.orderId = row.id
@@ -2100,6 +2118,10 @@
 
   onMounted(() => {
     void ensureCategoryPathDataLoaded()
+    const kw = route.query.keyword
+    if (typeof kw === 'string' && kw.trim()) {
+      searchForm.keyword = kw.trim()
+    }
     loadData()
     loadRegions()
     loadSystemDefaultsForAssign()

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