load(true)}>重试}
+ />
+ ) : null}
+
+ {freshness && (freshness.coversDeadline === false || freshness.stale) ? (
+
+ ) : null}
+
+
+
+
+ 前一日发货
+
+ )}
+ value={notStarted ? 0 : (summary.shippedOrders ?? 0)}
+ suffix="单"
+ formatter={(v) => (notStarted ? '—' : number(v))}
+ />
+
+ 共 {notStarted ? '—' : number(summary.packageCount)} 个包裹
+
+
+
+
+ 转运异常
+
+ )}
+ value={notStarted ? 0 : (summary.exceptionOrders ?? 0)}
+ suffix="单"
+ formatter={(v) => (notStarted ? '—' : number(v))}
+ valueStyle={{ color: (summary.exceptionOrders || 0) > 0 ? '#cf1322' : undefined }}
+ />
+
+ 涉及 {notStarted ? '—' : number(summary.exceptionPackages)} 个包裹
+
+
+
+ (notStarted ? '—' : number(v, 1))}
+ valueStyle={{ color: (summary.exceptionRate || 0) >= 10 ? '#d46b08' : undefined }}
+ />
+
+ 异常单 ÷ 前一日发货单
+
+
+
+ (notStarted ? '—' : number(v))}
+ />
+
+ 有转运异常的仓
+
+
+
+
+ {types.length ? (
+
+ {types.map((t) => {
+ const count = summary.byType?.[t.key] || 0;
+ const isActive = activeType === t.key;
+ return (
+
+
+
+ );
+ })}
+ {activeType ? (
+
+ ) : null}
+
+ ) : null}
+
+
+
点仓库看明细}
+ >
+
+
+ {centers.map((c) => {
+ const isActive = activeCenter === c.key;
+ return (
+ -
+
+
+
+
+ );
+ })}
+
+
+
+
+
+ 转运异常明细
+
+ {[activeCenterName, activeType ? types.find((t) => t.key === activeType)?.label : '']
+ .filter(Boolean).join(' · ') || '全部仓库'}
+ {' · '}
+ {detailOrders.length} 条
+ {data?.truncated && allOrders.length >= (data?.detailLimit || 0)
+ ? `(已截断,仅显示前 ${data.detailLimit} 条)`
+ : ''}
+
+
+ )}
+ extra={(activeCenter || activeType) ? (
+
+ ) : null}
+ >
+
+ {notStarted ? (
+
+ ) : detailOrders.length ? (
+ `${r.oId}-${r.trackingNo || r.sendTime}-${r.exceptionType}`}
+ size="small"
+ pagination={{ pageSize: 20, size: 'small', showSizeChanger: false, showTotal: (t) => `共 ${t} 条` }}
+ dataSource={detailOrders}
+ columns={DETAIL_COLUMNS}
+ scroll={{ x: 1900 }}
+ />
+ ) : (
+
+ )}
+
+
+
+
+
+ 说明:每天 09:00 核验前一日已发货订单的物流记录,只看两个卡点 ——
+ 未揽收(快递没来揽)和已揽收未中转(揽了但没有下一步中转记录,卡在网点),
+ 外加 无物流单 / 快递报异常 / 退回。
+ 运输中、派送中、已签收一律正常:「运输」字样之后的中转记录本来就不回传聚水潭,不作考核。
+ 子仓并入主仓统计;京仓-横林二仓京东云仓 与各代发仓不在 12 个发货仓范围内。
+
+ 不参与考核:
+ {exclusions?.carriers?.length ? (
+ <>承运商 {exclusions.carriers.join(' / ')}(这些专线轨迹不回传聚水潭或本身无快递轨迹);>
+ ) : null}
+ {exclusions?.conditionalCarriers?.length ? (
+ <>
+ {exclusions.conditionalCarriers.map((r) => (
+
+ {r.keyword} 只在 {(r.keepCenters || []).join(' / ')} 考核,
+ 其它仓异地用同一账号打单的剔出({r.reason});
+
+ ))}
+ >
+ ) : null}
+ 已取消 / 作废 / 拦截的订单
+ {exclusions?.orderLabelKeywords?.length
+ ? <>(含标签「{exclusions.orderLabelKeywords.join('」「')}」)>
+ : null}
+ ;{exclusions?.orderTypeLabels?.length ? exclusions.orderTypeLabels.join(' / ') : '刷单 / 信誉单'}
+ (按订单标签「刷单」「转空包」识别)—— 都不是真实发货,不用跟物流。
+ {exclusions && (exclusions.carrierOrders > 0 || exclusions.voidedOrders > 0
+ || exclusions.nonRealOrders > 0) ? (
+ <>
+ {' '}本批已剔除 {(exclusions.carrierOrders || 0) + (exclusions.voidedOrders || 0)
+ + (exclusions.nonRealOrders || 0)} 单
+ (承运商 {exclusions.carrierOrders || 0} 单、已终止 {exclusions.voidedOrders || 0} 单、
+ 刷单/信誉单 {exclusions.nonRealOrders || 0} 单),
+ {exclusions.carrierReasons && Object.keys(exclusions.carrierReasons).length ? (
+ <>
+ 其中承运商剔除:
+ {Object.entries(exclusions.carrierReasons)
+ .sort((a, b) => b[1] - a[1])
+ .map(([k, v]) => `${k} ${v} 单`)
+ .join(' / ')}
+ ;
+ >
+ ) : null}
+ 剔除的单既不算异常也不算发货分母,所以异常率不会被稀释。
+ >
+ ) : null}
+
+ 页面按当前物流快照计算:核验时刻之后才更新的轨迹(比如 9 点后才揽收、才签收)会让对应异常自动消失,
+ 所以这里的数字是「到此刻为止还异常的」—— 想留档 09:00 那一刻的结论,需要每天 9 点落一份快照。
+
+
+ );
+}
+
+export default WarehouseLogisticsException;
diff --git a/frontend/src/pages/WarehouseLogisticsException/style.css b/frontend/src/pages/WarehouseLogisticsException/style.css
new file mode 100644
index 0000000..d6b2611
--- /dev/null
+++ b/frontend/src/pages/WarehouseLogisticsException/style.css
@@ -0,0 +1,278 @@
+.wle-page {
+ padding: 16px 20px 24px;
+}
+
+.wle-header {
+ margin-bottom: 14px;
+}
+
+.wle-header .ant-typography {
+ margin-bottom: 4px;
+}
+
+/* 口径说明:页面顶部是深色渐变,直接放浅色小字看不清,给一块浅底 */
+.wle-note {
+ display: block;
+ max-width: 1180px;
+ margin-top: 8px;
+ padding: 10px 14px;
+ background: var(--sycm-bg-container);
+ border: 1px solid var(--sycm-border-color);
+ border-radius: 8px;
+ font-size: 12px;
+ line-height: 1.75;
+}
+
+/* ============ 批次状态条 ============ */
+.wle-status {
+ margin-bottom: 12px;
+}
+
+.wle-status__row {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+ flex-wrap: wrap;
+}
+
+.wle-status__batch {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ flex-wrap: wrap;
+}
+
+.wle-status__badge {
+ padding: 1px 8px;
+ border-radius: 4px;
+ background: rgba(22, 119, 255, 0.12);
+ color: #0958d9;
+ font-size: 12px;
+ font-weight: 600;
+ line-height: 20px;
+}
+
+.wle-status__badge--idle {
+ background: rgba(136, 135, 128, 0.14);
+ color: var(--sycm-text-tertiary);
+}
+
+.wle-status__label {
+ font-size: 13px;
+ color: var(--sycm-text-primary);
+}
+
+.wle-status__window,
+.wle-status__time {
+ font-size: 12px;
+ color: var(--sycm-text-tertiary);
+}
+
+.wle-alert {
+ margin-bottom: 12px;
+}
+
+/* ============ 统计卡 ============ */
+.wle-stats {
+ display: grid;
+ grid-template-columns: repeat(4, minmax(0, 1fr));
+ gap: 12px;
+ margin-bottom: 12px;
+}
+
+@media (max-width: 1200px) {
+ .wle-stats { grid-template-columns: repeat(2, minmax(0, 1fr)); }
+}
+
+.wle-stat {
+ height: 100%;
+}
+
+.wle-stat__sub {
+ display: block;
+ margin-top: 4px;
+ font-size: 12px;
+}
+
+/* ============ 异常类型筛选 ============ */
+.wle-types {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ flex-wrap: wrap;
+ margin-bottom: 12px;
+}
+
+.wle-type {
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+ padding: 2px 10px 2px 2px;
+ border: 1px solid var(--sycm-border-color);
+ border-radius: 16px;
+ background: var(--sycm-bg-container);
+ cursor: pointer;
+ transition: border-color 0.15s, background 0.15s;
+}
+
+.wle-type:hover {
+ border-color: rgba(22, 119, 255, 0.45);
+}
+
+.wle-type.is-active {
+ border-color: rgba(22, 119, 255, 0.6);
+ background: rgba(22, 119, 255, 0.08);
+}
+
+.wle-type.is-empty {
+ opacity: 0.55;
+}
+
+.wle-type__count {
+ font-size: 12px;
+ font-weight: 600;
+ color: var(--sycm-text-primary);
+ font-variant-numeric: tabular-nums;
+}
+
+.wle-type__clear {
+ padding: 0;
+ border: none;
+ background: transparent;
+ font-size: 12px;
+ color: #1677ff;
+ cursor: pointer;
+}
+
+/* ============ 主从两栏 ============ */
+.wle-body {
+ display: flex;
+ align-items: flex-start;
+ gap: 14px;
+}
+
+.wle-centers {
+ flex: none;
+ width: 272px;
+}
+
+.wle-centers__hint {
+ font-size: 11px;
+}
+
+.wle-centers__list {
+ margin: 0;
+ padding: 0;
+ list-style: none;
+}
+
+.wle-center {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 8px;
+ width: 100%;
+ padding: 6px 8px;
+ margin-bottom: 2px;
+ border: 1px solid transparent;
+ border-radius: 6px;
+ background: transparent;
+ font-size: 13px;
+ text-align: left;
+ color: var(--sycm-text-primary);
+ cursor: pointer;
+ transition: background 0.12s ease;
+}
+
+.wle-center:hover {
+ background: var(--sycm-bg-subtle);
+}
+
+.wle-center.is-active {
+ border-color: rgba(22, 119, 255, 0.45);
+ background: rgba(22, 119, 255, 0.08);
+}
+
+.wle-center.is-clean .wle-center__name {
+ color: var(--sycm-text-tertiary);
+}
+
+.wle-center__name {
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+}
+
+.wle-center__badge {
+ flex: none;
+ min-width: 24px;
+ padding: 0 6px;
+ border-radius: 9px;
+ background: rgba(136, 135, 128, 0.12);
+ font-size: 11px;
+ font-weight: 600;
+ line-height: 17px;
+ text-align: center;
+ color: var(--sycm-text-tertiary);
+ font-variant-numeric: tabular-nums;
+}
+
+.wle-center__badge.is-warn {
+ background: rgba(255, 77, 79, 0.16);
+ color: #cf1322;
+}
+
+.wle-detail {
+ flex: 1 1 auto;
+ min-width: 0;
+}
+
+.wle-detail__scope {
+ margin-left: 8px;
+ font-size: 12px;
+ font-weight: 400;
+ color: var(--sycm-text-tertiary);
+}
+
+.wle-detail__clear {
+ padding: 0;
+ border: none;
+ background: transparent;
+ font-size: 12px;
+ color: #1677ff;
+ cursor: pointer;
+}
+
+.wle-detail__hours {
+ color: #cf1322;
+ font-variant-numeric: tabular-nums;
+}
+
+.wle-detail__soid {
+ font-variant-numeric: tabular-nums;
+}
+
+.wle-foot {
+ display: block;
+ margin-top: 12px;
+ font-size: 12px;
+ color: var(--sycm-text-tertiary);
+}
+
+/* 窄屏:仓库列表改两列网格,明细整行下移 */
+@media (max-width: 1200px) {
+ .wle-body {
+ flex-direction: column;
+ }
+
+ .wle-centers {
+ width: 100%;
+ }
+
+ .wle-centers__list {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 2px 10px;
+ }
+}
diff --git a/frontend/src/router/index.jsx b/frontend/src/router/index.jsx
index 8d537c5..82f03c3 100644
--- a/frontend/src/router/index.jsx
+++ b/frontend/src/router/index.jsx
@@ -20,6 +20,7 @@ const ProductMonitoring = lazy(() => import('../pages/ProductMonitoring'));
const ModularStockMonitor = lazy(() => import('../pages/ModularStockMonitor'));
const MkhBomData = lazy(() => import('../pages/MkhBomData'));
const WarehouseShipTimeliness = lazy(() => import('../pages/WarehouseShipTimeliness'));
+const WarehouseLogisticsException = lazy(() => import('../pages/WarehouseLogisticsException'));
const DailyInventory = lazy(() => import('../pages/DailyInventory'));
const StockSalesAnalysis = lazy(() => import('../pages/StockSalesAnalysis'));
const PickingBinSalesAnalysis = lazy(() => import('../pages/PickingBinSalesAnalysis'));
@@ -120,6 +121,10 @@ const router = createBrowserRouter([
path: 'planning/operation/warehouse-ship-timeliness',
element: renderLazyComponent(WarehouseShipTimeliness),
},
+ {
+ path: 'planning/operation/warehouse-logistics-exception',
+ element: renderLazyComponent(WarehouseLogisticsException),
+ },
{
path: 'planning/operation/daily-inventory',
element: renderLazyComponent(DailyInventory),
--
2.47.2