Merge pull request 'refactor: 移除大货库存监控功能(26 仓)' (#29) from codex/remove-large-stock-monitor-0923 into main

This commit is contained in:
youruo 2026-09-23 16:12:50 +08:00
commit d52151d7dc
13 changed files with 0 additions and 1698 deletions

View File

@ -1,113 +0,0 @@
// 端到端校验:大货库存监控 26 仓在「数据截止时刻(快照日次日 01:00)」口径下的取数
// 用法:node scripts/verify-large-stock-monitor.js
process.env.NODE_ENV = process.env.NODE_ENV || 'development';
require('dotenv').config({ path: require('path').resolve(__dirname, '../.env') });
const http = require('http');
const BASE = 'http://127.0.0.1:4001';
const { LARGE_STOCK_MONITOR_WAREHOUSES } = require('../src/config/largeStockMonitorConfig');
function request(method, path, token, body) {
return new Promise((resolve, reject) => {
const payload = body ? JSON.stringify(body) : null;
const req = http.request(
`${BASE}${path}`,
{
method,
headers: {
'Content-Type': 'application/json',
...(token ? { Authorization: `Bearer ${token}` } : {}),
...(payload ? { 'Content-Length': Buffer.byteLength(payload) } : {}),
},
},
(res) => {
let data = '';
res.on('data', (c) => (data += c));
res.on('end', () => {
try {
resolve({ status: res.statusCode, json: JSON.parse(data) });
} catch (e) {
reject(new Error(`响应解析失败 ${res.statusCode}: ${data.slice(0, 200)}`));
}
});
}
);
req.on('error', reject);
if (payload) req.write(payload);
req.end();
});
}
(async () => {
const login = await request('POST', '/api/auth/local-preview', null, {});
const token = login.json?.token || login.json?.data?.token || login.json?.access_token;
if (!token) {
console.error('登录失败:', JSON.stringify(login.json).slice(0, 300));
process.exit(1);
}
console.log('登录成功\n');
let ok = 0;
const problems = [];
let sampleAsOf = null;
for (const w of LARGE_STOCK_MONITOR_WAREHOUSES) {
const url = `/api/large-stock-monitor/overview?warehouse=${encodeURIComponent(w.warehouse)}`;
try {
const res = await request('GET', url, token);
const d = res.json?.data;
if (!res.json?.success || !d) {
problems.push(`${w.warehouse}: 接口失败 ${res.status} ${res.json?.error || ''}`);
continue;
}
sampleAsOf = sampleAsOf || d.asOf;
const o = d.overview || {};
const filled = Object.values(o).filter((v) => v !== '—' && v !== null && v !== undefined).length;
if (!d.hasData) {
problems.push(`${w.warehouse}: 无数据源 (source=${d.source})`);
continue;
}
ok += 1;
console.log(
`[${String(w.idx).padStart(2)}] ${w.warehouse}\n` +
` 快照=${d.latestBizDate} 截止=${d.asOf} 区间=${d.period?.start} ~ ${d.period?.end}\n` +
` SKU=${d.skuCount} 期末=${o.closingQty} 期初(估)=${o.periodStart}\n` +
` 采购进=${o.purchaseIn} 采购退=${o.purchaseReturn} 销售出=${o.salesOut} 销售退=${o.salesReturn}\n` +
` 周转=${o.turnoverDays} 预警=${o.warnLevel} 慢流=${o.slowLevel} 呆滞=${o.staleLevel} 僵尸=${o.zombieLevel}\n` +
` 有值列 ${filled}/18 source=${d.source}`
);
} catch (e) {
problems.push(`${w.warehouse}: ${e.message}`);
}
}
// 明细接口校验:带 @asOf 的分级筛选 SQL(level 过滤会引用 @asOf)
console.log('\n===== 明细接口校验 =====');
for (const w of LARGE_STOCK_MONITOR_WAREHOUSES.slice(0, 4)) {
for (const lv of ['', 'warn', 'slow', 'stale', 'zombie']) {
const qs = `warehouse=${encodeURIComponent(w.warehouse)}&page=1&pageSize=5${lv ? `&level=${lv}` : ''}`;
try {
const res = await request('GET', `/api/large-stock-monitor/sku-list?${qs}`, token);
const d = res.json?.data;
if (!res.json?.success) {
problems.push(`${w.warehouse} level=${lv || '全部'}: ${res.json?.error}`);
} else {
console.log(` ${w.warehouse} level=${lv || '全部'} -> total=${d?.total} 返回=${d?.items?.length} 截止=${d?.asOf}`);
}
} catch (e) {
problems.push(`${w.warehouse} level=${lv}: ${e.message}`);
}
}
}
console.log(`\n===== 汇总 =====`);
console.log(`数据截止时刻(asOf): ${sampleAsOf}`);
console.log(`有数据仓库: ${ok}/${LARGE_STOCK_MONITOR_WAREHOUSES.length}`);
if (problems.length) {
console.log('异常:');
problems.forEach((p) => console.log(' - ' + p));
} else {
console.log('全部仓库取数正常,无异常');
}
process.exit(0);
})();

View File

@ -1,514 +0,0 @@
// 大货库存监控 - 后端路由
// 业务数据源:rpt.pm_stockMonitor(MSSQL,业务库 JSReportReplica)
// rpt.jst_*(聚水潭单据/库存表,用于出入库流水)
// 配置源:C:/Users/.../Desktop/库存监控.xlsx 中的 26 行仓库配置
//
// 计算口径(用户给定):
// 月度发货总件数 ≈ threeAvgSale × 30 (业务库无直接月度发货字段,按 3 天日均销量 × 30 估算)
// 月度日均库存件数 ≈ jsStock (业务库只有当前日快照,作为日均近似)
// 库存周转天数 = 月度发货总件数 / 月度日均库存件数 × 100%
// 注:按公式实际为周转率(%),列名沿用 Excel 表头"库存周转天数"
// 库龄(月) = DATEDIFF(MONTH, created, GETDATE())
//
// 分级口径(用户给定,按"库龄 + 周转天数(%)"两个维度):
// 滞销预警 warn 库龄 < 3 个月 且 30 ≤ 周转天数 ≤ 199
// 商品慢流 slow 库龄 < 3 个月 且 周转天数 > 200
// 或 库龄 > 3 个月 且 30 ≤ 周转天数 ≤ 99
// 商品呆滞 stale 库龄 3~12 个月 且 周转天数 ≥ 100
// 或 库龄 > 12 个月 且 100 ≤ 周转天数 ≤ 199
// 商品僵尸 zombie 库龄 > 12 个月 且 周转天数 ≥ 200
const express = require('express');
const authMiddleware = require('../middleware/auth');
const cloudDb = require('../config/cloudDatabase');
const { LARGE_STOCK_MONITOR_WAREHOUSES } = require('../config/largeStockMonitorConfig');
const { fetchFlowSummary, fetchInventoryQty } = require('../services/largeStockMonitorFlow');
const router = express.Router();
const LEVEL_LABELS = {
warn: '滞销预警',
slow: '商品慢流',
stale: '商品呆滞',
zombie: '商品僵尸',
};
// 18 列表头(与库存监控.xlsx 第 1 行的 D..U 列对应)
// available=false 表示业务库无对应数据源(加工/盘点/调拨/其它出库),前端显示「—」
const COLUMN_HEADERS = [
{ key: 'periodStart', label: '期初数据', available: true, estimated: true },
{ key: 'processIn', label: '加工进仓', available: false },
{ key: 'purchaseReturn', label: '采购退货', available: true },
{ key: 'purchaseIn', label: '采购进仓', available: true },
{ key: 'salesOut', label: '销售出仓', available: true },
{ key: 'stocktake', label: '盘点', available: false },
{ key: 'processOut', label: '加工出仓', available: false },
{ key: 'transferOut', label: '调拨出', available: false },
{ key: 'salesReturn', label: '销售退货', available: true },
{ key: 'transferIn', label: '调拨入', available: false },
{ key: 'openingBalance', label: '期初', available: true, estimated: true },
{ key: 'otherOut', label: '其它出库', available: false },
{ key: 'closingQty', label: '期末数量', available: true },
{ key: 'turnoverDays', label: '库存周转天数', available: true },
{ key: 'warnLevel', label: '滞销预警级库存量', available: true },
{ key: 'slowLevel', label: '商品慢流级库存量', available: true },
{ key: 'staleLevel', label: '商品呆滞级库存量', available: true },
{ key: 'zombieLevel', label: '商品僵尸级库存量', available: true },
];
// 在 SELECT/WHERE 中复用:库龄月数、月度发货、月度日均库存、周转天数(%)、分级
// 关键:所有算式都做 NULL/0 兜底,避免分母为 0 导致 NULL 污染整列
// 时间基准统一使用 @asOf(数据截止时刻,默认快照日次日 01:00),不再用 GETDATE(),
// 保证同一批报表内库龄/分级与流水口径完全一致。
function buildDerivedColumns(alias = 'm') {
const a = alias ? `${alias}.` : '';
const T = 'CONVERT(DATETIME, @asOf, 120)';
return {
ageMonths: `CASE WHEN ${a}created IS NULL THEN NULL ELSE CAST(DATEDIFF(DAY, ${a}created, ${T}) AS DECIMAL(18, 4)) / 30.0 END`,
monthlyShipment: `(ISNULL(${a}threeAvgSale, 0) * 30.0)`,
avgDailyStock: `CAST(ISNULL(${a}jsStock, 0) AS DECIMAL(18, 4))`,
turnoverDays: `CASE WHEN ISNULL(${a}jsStock, 0) = 0 THEN NULL ELSE CAST(ISNULL(${a}threeAvgSale, 0) * 30.0 * 100.0 / ${a}jsStock AS DECIMAL(18, 4)) END`,
levelExpr: `
CASE
WHEN ${a}created IS NULL OR ISNULL(${a}jsStock, 0) = 0 OR ISNULL(${a}threeAvgSale, 0) * 30.0 / NULLIF(${a}jsStock, 0) IS NULL THEN NULL
WHEN DATEDIFF(DAY, ${a}created, ${T}) < 90 AND (ISNULL(${a}threeAvgSale, 0) * 30.0 * 100.0 / ${a}jsStock) BETWEEN 30 AND 199 THEN 'warn'
WHEN DATEDIFF(DAY, ${a}created, ${T}) < 90 AND (ISNULL(${a}threeAvgSale, 0) * 30.0 * 100.0 / ${a}jsStock) >= 200 THEN 'slow'
WHEN DATEDIFF(DAY, ${a}created, ${T}) >= 90 AND (ISNULL(${a}threeAvgSale, 0) * 30.0 * 100.0 / ${a}jsStock) BETWEEN 30 AND 99 THEN 'slow'
WHEN DATEDIFF(DAY, ${a}created, ${T}) BETWEEN 90 AND 365 AND (ISNULL(${a}threeAvgSale, 0) * 30.0 * 100.0 / ${a}jsStock) >= 100 THEN 'stale'
WHEN DATEDIFF(DAY, ${a}created, ${T}) > 365 AND (ISNULL(${a}threeAvgSale, 0) * 30.0 * 100.0 / ${a}jsStock) BETWEEN 100 AND 199 THEN 'stale'
WHEN DATEDIFF(DAY, ${a}created, ${T}) > 365 AND (ISNULL(${a}threeAvgSale, 0) * 30.0 * 100.0 / ${a}jsStock) >= 200 THEN 'zombie'
ELSE NULL
END
`,
};
}
function resolveWarehouse(warehouse) {
const target = String(warehouse || '').trim();
if (!target) return null;
return LARGE_STOCK_MONITOR_WAREHOUSES.find((entry) => entry.warehouse === target) || null;
}
function buildPlaceholderRow() {
return COLUMN_HEADERS.reduce((acc, column) => {
acc[column.key] = '—';
return acc;
}, {});
}
// 默认跟随业务库最新快照日期;可通过 ?date=YYYY-MM-DD 指定某一天
const DEFAULT_TARGET_DATE = null;
// 数据截止时刻:快照日次日的凌晨 01:00。
// 业务库每天凌晨 1 点跑批生成快照,标注为前一日(T-1)的库存,
// 因此「9 月 23 日凌晨 1 点的数据」= 快照日 2026-09-22 + 截止时刻 2026-09-23 01:00。
const AS_OF_TIME = '01:00';
// 流水统计区间:快照所在自然月的 1 日 00:00 ~ 快照日次日 01:00(左闭右开)
// 即完整覆盖快照日全天,并包含跑批当日凌晨 0 点~1 点的单据
function buildPeriodRange(bizDate) {
if (!bizDate || !/^\d{4}-\d{2}-\d{2}$/.test(bizDate)) return null;
const [y, mo] = bizDate.split('-');
const start = `${y}-${mo}-01 00:00`;
const d = new Date(`${bizDate}T00:00:00Z`);
d.setUTCDate(d.getUTCDate() + 1);
const endDay = d.toISOString().slice(0, 10);
const end = `${endDay} ${AS_OF_TIME}`;
return { start, end, endDay, asOf: end };
}
// 允许 ?asOf=YYYY-MM-DD HH:mm 手工覆盖截止时刻(排查/补数用)
function normalizeAsOf(value) {
const raw = String(value || '').trim();
if (!raw) return null;
if (/^\d{4}-\d{2}-\d{2}$/.test(raw)) return `${raw} ${AS_OF_TIME}`;
const m = raw.match(/^(\d{4}-\d{2}-\d{2})[ T](\d{2}:\d{2})/);
if (m) return `${m[1]} ${m[2]}`;
return null;
}
const pad2 = (n) => String(n).padStart(2, '0');
function localDateString(d) {
return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`;
}
function localDateTimeString(d) {
return `${localDateString(d)} ${pad2(d.getHours())}:${pad2(d.getMinutes())}`;
}
// 无库存快照的仓库(走实时库存兜底)没有「快照日次日」可用,截止时刻取今天凌晨 1 点;
// 若当前还没到凌晨 1 点(跑批尚未完成),则退化为当前时刻。
function fallbackAsOf() {
const now = new Date();
const hourFloor = Number(AS_OF_TIME.split(':')[0]);
if (now.getHours() >= hourFloor) return `${localDateString(now)} ${AS_OF_TIME}`;
return localDateTimeString(now);
}
async function fetchTargetBizDate(dbWarehouse, preferredDate) {
if (!dbWarehouse) return null;
const rows = await cloudDb.query(
`SELECT TOP 1 CONVERT(VARCHAR(10), date, 23) AS d
FROM rpt.pm_stockMonitor
WHERE warehouse = @warehouse AND date IS NOT NULL
AND (@preferredDate IS NULL OR CONVERT(VARCHAR(10), date, 23) <= @preferredDate)
ORDER BY date DESC`,
{ warehouse: dbWarehouse, preferredDate: preferredDate || null }
);
return rows[0]?.d || null;
}
// 期初倒推:期末 − 采购进仓 + 采购退货 + 销售出仓 − 销售退货
// 业务库 pm_stockMonitor 仅保留最新单日快照(无上月末历史),且加工/调拨/盘点/其它出库无数据源,
// 因此期初只能由已知流水倒推,属估算值(前端标注「估」)。
// 倒推出现负数说明该仓库本期存在未采集到的出库(调拨/加工/其它出库),此时不再展示估算值,
// 避免出现「负库存」这类明显失真的数字。
function estimateOpeningBalance(closingQty, flow) {
if (closingQty == null) return null;
const purchaseIn = flow?.purchaseIn ?? 0;
const purchaseReturn = flow?.purchaseReturn ?? 0;
const salesOut = flow?.salesOut ?? 0;
const salesReturn = flow?.salesReturn ?? 0;
const value = closingQty - purchaseIn + purchaseReturn + salesOut - salesReturn;
return value >= 0 ? value : null;
}
router.get('/config', authMiddleware, async (_req, res) => {
try {
res.json({
success: true,
data: {
columns: COLUMN_HEADERS,
levelLabels: LEVEL_LABELS,
warehouses: LARGE_STOCK_MONITOR_WAREHOUSES.map((entry) => ({
warehouse: entry.warehouse,
group: entry.group,
routeKey: entry.routeKey,
hasData: Boolean(entry.dbWarehouse),
hasWms: Boolean(entry.wmsCoId),
})),
},
});
} catch (error) {
console.error('[LargeStockMonitor] 获取配置失败:', error);
res.status(500).json({ success: false, error: error.message || '获取配置失败' });
}
});
router.get('/overview', authMiddleware, async (req, res) => {
try {
const config = resolveWarehouse(req.query.warehouse);
if (!config) {
return res.status(404).json({ success: false, error: '未知的仓库配置' });
}
const requestedDate = String(req.query.date || '').trim() || null;
if (requestedDate && !/^\d{4}-\d{2}-\d{2}$/.test(requestedDate)) {
return res.status(400).json({ success: false, error: 'date 参数格式应为 YYYY-MM-DD' });
}
const dbWarehouse = config.dbWarehouse;
const wmsCoId = config.wmsCoId;
const overview = buildPlaceholderRow();
const baseResponse = {
warehouse: config.warehouse,
group: config.group,
routeKey: config.routeKey,
requestedDate,
latestBizDate: null,
fallbackDate: null,
asOf: null,
period: null,
source: null,
hasData: false,
skuCount: 0,
overview,
};
// 1) 快照日期:优先 pm_stockMonitor;无该仓库时退回聚水潭实时库存(无日期概念)
let latestBizDate = dbWarehouse
? await fetchTargetBizDate(dbWarehouse, requestedDate).catch(() => null)
: null;
const fallbackDate = requestedDate && latestBizDate && latestBizDate !== requestedDate ? latestBizDate : null;
const period = buildPeriodRange(latestBizDate);
// 数据截止时刻:默认快照日次日 01:00(如 2026-09-23 01:00),可用 ?asOf= 覆盖
const asOf = normalizeAsOf(req.query.asOf) || period?.asOf || fallbackAsOf();
// 2) 出入库流水(本月 1 日 ~ 截止时刻);pm_stockMonitor 无该仓库时用当天所在月
const today = localDateString(new Date());
const periodForFlow = period
? { ...period, end: asOf || period.end }
: { start: buildPeriodRange(today)?.start, end: asOf };
const flow = await fetchFlowSummary({
wmsCoId,
dbWarehouse: dbWarehouse || config.warehouse.replace(/主仓$/, ''),
periodStart: periodForFlow?.start,
periodEnd: periodForFlow?.end,
}).catch(() => ({ purchaseIn: null, purchaseReturn: null, salesOut: null, salesReturn: null }));
if (flow.purchaseReturn != null) overview.purchaseReturn = flow.purchaseReturn;
if (flow.purchaseIn != null) overview.purchaseIn = flow.purchaseIn;
if (flow.salesOut != null) overview.salesOut = flow.salesOut;
if (flow.salesReturn != null) overview.salesReturn = flow.salesReturn;
// 3) 期末 / 分级
// 期末数量统一取「聚水潭实际库存」rpt.jst_inventoryInfo(按 wms_co_id 单仓统计):
// pm_stockMonitor 的 warehouse 是「归属仓」而非实物所在仓,其 jsStock 会把
// 已调拨到外仓(如横林二仓)的货仍计在本仓名下,与仓库实物库存对不上
// (例:崔桥工厂仓 快照 jsStock 115135 vs 实物 92439)。
// 库龄/周转率/四级库存量只能由 pm_stockMonitor 提供(实时库存表无 created、销量字段)。
let skuCount = 0;
let source = null;
let closingSource = null;
let hasGrade = false;
let summary = {};
if (dbWarehouse && latestBizDate) {
const D = buildDerivedColumns();
const summaryRows = await cloudDb.query(
`SELECT
COUNT(*) AS skuCount,
SUM(CAST(jsStock AS DECIMAL(18, 2))) AS totalStock,
AVG(${D.turnoverDays}) AS avgTurnover,
MAX(${D.turnoverDays}) AS maxTurnover,
SUM(CASE WHEN ${D.levelExpr} = 'warn' THEN CAST(jsStock AS DECIMAL(18, 2)) ELSE 0 END) AS warnLevel,
SUM(CASE WHEN ${D.levelExpr} = 'slow' THEN CAST(jsStock AS DECIMAL(18, 2)) ELSE 0 END) AS slowLevel,
SUM(CASE WHEN ${D.levelExpr} = 'stale' THEN CAST(jsStock AS DECIMAL(18, 2)) ELSE 0 END) AS staleLevel,
SUM(CASE WHEN ${D.levelExpr} = 'zombie' THEN CAST(jsStock AS DECIMAL(18, 2)) ELSE 0 END) AS zombieLevel
FROM rpt.pm_stockMonitor m
WHERE m.warehouse = @warehouse
AND CONVERT(VARCHAR(10), m.date, 23) = @bizDate`,
{ warehouse: dbWarehouse, bizDate: latestBizDate, asOf }
);
summary = summaryRows[0] || {};
const snapSkuCount = Number(summary.skuCount || 0);
if (snapSkuCount > 0) {
hasGrade = true;
overview.turnoverDays = summary.avgTurnover == null ? null : Number(summary.avgTurnover);
overview.warnLevel = Number(summary.warnLevel || 0);
overview.slowLevel = Number(summary.slowLevel || 0);
overview.staleLevel = Number(summary.staleLevel || 0);
overview.zombieLevel = Number(summary.zombieLevel || 0);
}
}
// 3.2 期末数量:聚水潭实际库存(单仓 wms_co_id);SKU 数同步跟随期末口径
let closingTakenAt = null;
if (wmsCoId) {
const inv = await fetchInventoryQty(wmsCoId).catch(() => null);
if (inv && inv.qty != null) {
closingSource = 'jst_inventoryInfo';
overview.closingQty = inv.qty;
skuCount = inv.skuCount || 0;
closingTakenAt = inv.takenAt || null;
}
}
// 3.3 兜底:拿不到实时库存时退回快照归属库存(并在响应中标记来源)
if (!closingSource && summary.totalStock != null && Number(summary.skuCount || 0) > 0) {
closingSource = 'pm_stockMonitor';
overview.closingQty = Number(summary.totalStock || 0);
skuCount = Number(summary.skuCount || 0);
}
source = closingSource || (hasGrade ? 'pm_stockMonitor' : null);
// 四级库存量基于快照归属库存分级,与期末(实际库存)口径不同,供前端提示用
const gradeBasisStock = Number(summary.totalStock || 0);
// 4) 期初倒推(估算)
const opening = estimateOpeningBalance(
typeof overview.closingQty === 'number' ? overview.closingQty : null,
flow
);
if (opening != null) {
overview.periodStart = opening;
overview.openingBalance = opening;
}
res.json({
success: true,
data: {
...baseResponse,
latestBizDate: latestBizDate || null,
fallbackDate,
asOf,
period: periodForFlow,
source,
closingSource,
closingTakenAt,
hasGrade,
gradeBasisStock,
hasData: Boolean(source),
skuCount,
overview,
turnoverDaysAvg: summary.avgTurnover != null ? Number(summary.avgTurnover) : null,
turnoverDaysMax: summary.maxTurnover != null ? Number(summary.maxTurnover) : null,
},
});
} catch (error) {
console.error('[LargeStockMonitor] 获取概览失败:', error);
res.status(error.statusCode || 500).json({
success: false,
error: error.message || '获取概览失败',
});
}
});
router.get('/sku-list', authMiddleware, async (req, res) => {
try {
const config = resolveWarehouse(req.query.warehouse);
if (!config) {
return res.status(404).json({ success: false, error: '未知的仓库配置' });
}
const requestedDate = String(req.query.date || '').trim() || null;
if (requestedDate && !/^\d{4}-\d{2}-\d{2}$/.test(requestedDate)) {
return res.status(400).json({ success: false, error: 'date 参数格式应为 YYYY-MM-DD' });
}
const dbWarehouse = config.dbWarehouse;
const wmsCoId = config.wmsCoId;
const page = Math.max(parseInt(req.query.page, 10) || 1, 1);
const pageSize = Math.min(Math.max(parseInt(req.query.pageSize, 10) || 20, 1), 200);
const offset = (page - 1) * pageSize;
const level = String(req.query.level || '').trim();
const keyword = String(req.query.keyword || '').trim();
const baseResponse = {
warehouse: config.warehouse,
group: config.group,
requestedDate,
latestBizDate: null,
fallbackDate: null,
asOf: null,
source: null,
page,
pageSize,
total: 0,
items: [],
};
const latestBizDate = dbWarehouse
? await fetchTargetBizDate(dbWarehouse, requestedDate).catch(() => null)
: null;
const asOf = normalizeAsOf(req.query.asOf) || buildPeriodRange(latestBizDate)?.asOf || fallbackAsOf();
// pm_stockMonitor 有数据 → 走原有分级明细
if (dbWarehouse && latestBizDate) {
const D = buildDerivedColumns();
const filters = ['m.warehouse = @warehouse', "CONVERT(VARCHAR(10), m.date, 23) = @bizDate"];
const params = { warehouse: dbWarehouse, bizDate: latestBizDate, asOf };
if (keyword) {
filters.push('(m.sku LIKE @kw OR m.skuName LIKE @kw)');
params.kw = `%${keyword}%`;
}
if (['warn', 'slow', 'stale', 'zombie'].includes(level)) {
filters.push(`${D.levelExpr} = @level`);
params.level = level;
}
const whereClause = filters.join(' AND ');
const totalRows = await cloudDb.query(
`SELECT COUNT(*) AS total FROM rpt.pm_stockMonitor m WHERE ${whereClause}`,
params
);
const total = Number(totalRows[0]?.total || 0);
params.pageSize = pageSize;
params.offset = offset;
const items = await cloudDb.query(
`SELECT
m.sku, m.spu, m.skuName, m.supplier, m.purchase, m.category,
m.jsStock, m.threeAvgSale, m.created,
${D.ageMonths} AS ageMonths,
${D.monthlyShipment} AS monthlyShipment,
${D.avgDailyStock} AS avgDailyStock,
${D.turnoverDays} AS turnoverDays,
${D.levelExpr} AS level,
m.warehouse
FROM rpt.pm_stockMonitor m
WHERE ${whereClause}
ORDER BY m.jsStock DESC, m.sku ASC
OFFSET @offset ROWS FETCH NEXT @pageSize ROWS ONLY`,
params
);
return res.json({
success: true,
data: {
...baseResponse,
latestBizDate,
fallbackDate: requestedDate && latestBizDate !== requestedDate ? latestBizDate : null,
asOf,
source: 'pm_stockMonitor',
total,
items,
},
});
}
// 兜底:聚水潭实时库存明细(无库龄/销量字段,level 与周转率留空)
// 该数据源算不出库龄与周转率,任何分级筛选都不可能命中,直接返回空列表,
// 避免点选「商品僵尸」等时把整仓库存原样列出造成误导。
if (['warn', 'slow', 'stale', 'zombie'].includes(level)) {
return res.json({
success: true,
data: { ...baseResponse, asOf, source: 'jst_inventoryInfo', total: 0, items: [] },
});
}
if (!wmsCoId) {
return res.json({ success: true, data: baseResponse });
}
const invFilters = ['v.wms_co_id = @wmsCoId', 'ISNULL(v.qty, 0) <> 0'];
const invParams = { wmsCoId };
if (keyword) {
invFilters.push('(v.sku_id LIKE @kw OR v.name LIKE @kw)');
invParams.kw = `%${keyword}%`;
}
const invWhere = invFilters.join(' AND ');
const invTotal = await cloudDb.query(
`SELECT COUNT(*) AS total FROM rpt.jst_inventoryInfo v WHERE ${invWhere}`,
invParams
);
const total = Number(invTotal[0]?.total || 0);
invParams.pageSize = pageSize;
invParams.offset = offset;
const items = await cloudDb.query(
`SELECT
v.sku_id AS sku,
v.i_id AS spu,
v.name AS skuName,
v.qty AS jsStock,
CAST(NULL AS DECIMAL(18, 4)) AS ageMonths,
CAST(NULL AS DECIMAL(18, 4)) AS monthlyShipment,
CAST(v.qty AS DECIMAL(18, 4)) AS avgDailyStock,
CAST(NULL AS DECIMAL(18, 4)) AS turnoverDays,
CAST(NULL AS VARCHAR(10)) AS level,
w.name AS warehouse
FROM rpt.jst_inventoryInfo v
LEFT JOIN rpt.jst_warehouseInfo w ON w.wms_co_id = v.wms_co_id
WHERE ${invWhere}
ORDER BY v.qty DESC, v.sku_id ASC
OFFSET @offset ROWS FETCH NEXT @pageSize ROWS ONLY`,
invParams
);
res.json({
success: true,
data: { ...baseResponse, asOf, source: 'jst_inventoryInfo', total, items },
});
} catch (error) {
console.error('[LargeStockMonitor] 获取 SKU 列表失败:', error);
res.status(error.statusCode || 500).json({
success: false,
error: error.message || '获取 SKU 列表失败',
});
}
});
module.exports = router;

View File

@ -1,76 +0,0 @@
// 大货库存监控配置 - 数据源:C:/Users/15018737611/Desktop/库存监控.xlsx
// 共 26 行,按 Excel 顺序保留:序号 / 二级功能按键 / 仓库全名
// 业务库 rpt.pm_stockMonitor.warehouse 的命名规则与 Excel 不一致:
// Excel 通常带「主仓」后缀,业务库只保留「主」前那一段。
// 已在 dbWarehouse 字段手工映射出业务库 warehouse 精确值;无法对应的留 null。
// 菜单 label 显示 Excel 仓库全名,routeKey 用 slug 化的二级功能按键+序号保证唯一。
// wmsCoId 来自 rpt.jst_warehouseInfo(聚水潭仓库主数据,155 行,26 个仓库全部命中),
// 用于从出入库单据表(jst_purchase / jst_purchaseout / jst_salesOutboundInfo 等)取流水数量。
// dbWarehouse 用于 rpt.pm_stockMonitor;该报表仅覆盖 16 个仓库,其余为 null。
const RAW_WAREHOUSES = [
{ idx: 1, group: '半成品部件仓', warehouse: '简尚-崔桥工厂半成品包装项目仓主仓', dbWarehouse: '简尚-崔桥工厂半成品包装项目仓', wmsCoId: 15718196 },
{ idx: 2, group: '运营C', warehouse: '简尚-运营C崔桥工厂仓专用仓主仓', dbWarehouse: null, wmsCoId: 15301815 },
{ idx: 3, group: '崔桥工厂仓', warehouse: '简尚-崔桥工厂仓主仓', dbWarehouse: '简尚-崔桥工厂仓', wmsCoId: 15038426 },
{ idx: 4, group: '崔桥工厂仓', warehouse: '简尚-崔桥工厂京东仓主仓', dbWarehouse: null, wmsCoId: 15045345 },
{ idx: 5, group: '崔桥工厂品牌仓', warehouse: '简尚-崔桥工厂品牌仓主仓', dbWarehouse: '简尚-崔桥工厂品牌仓', wmsCoId: 15870495 },
{ idx: 6, group: '河南洛宁仓', warehouse: '简尚-河南洛宁仓主仓', dbWarehouse: '简尚-河南洛宁仓', wmsCoId: 15539084 },
{ idx: 7, group: '运营D', warehouse: '简尚-运营D专用仓库主仓', dbWarehouse: null, wmsCoId: 15051915 },
{ idx: 8, group: '运营C', warehouse: '简尚-运营C横林二仓专用仓主仓', dbWarehouse: null, wmsCoId: 15301814 },
{ idx: 9, group: '横林二仓', warehouse: '简尚-横林二仓主仓', dbWarehouse: '简尚-横林二仓', wmsCoId: 15033302 },
{ idx: 10, group: '横林三仓', warehouse: '简尚-横林三仓主仓', dbWarehouse: '简尚-横林三仓', wmsCoId: 15043561 },
{ idx: 11, group: '山东品牌仓', warehouse: '简尚-山东品牌仓主仓', dbWarehouse: '简尚-山东品牌仓', wmsCoId: 15182705 },
{ idx: 12, group: '山东青集岗仓', warehouse: '简尚-山东青岗集京东仓主仓', dbWarehouse: null, wmsCoId: 15406664 },
{ idx: 13, group: '山东青集岗仓', warehouse: '简尚-山东青岗集仓主仓', dbWarehouse: '简尚-山东青岗集仓', wmsCoId: 15137523 },
{ idx: 14, group: '运营D', warehouse: '简尚-运营D苏北专用仓主仓', dbWarehouse: null, wmsCoId: 15406876 },
{ idx: 15, group: '运营C', warehouse: '简尚-运营C新苏北专用仓主仓', dbWarehouse: null, wmsCoId: 15301812 },
{ idx: 16, group: '苏北仓', warehouse: '简尚-新苏北仓主仓', dbWarehouse: '简尚-新苏北仓', wmsCoId: 15038347 },
{ idx: 17, group: '分销仓', warehouse: '简尚-分销部苏北专用仓主仓', dbWarehouse: '简尚-分销部苏北专用仓', wmsCoId: 15045325 },
{ idx: 18, group: '运营D', warehouse: '简尚-运营D睢宁专用仓主仓', dbWarehouse: null, wmsCoId: 15051907 },
{ idx: 19, group: '运营C', warehouse: '简尚-运营C睢宁专用仓主仓', dbWarehouse: null, wmsCoId: 15301813 },
{ idx: 20, group: '睢宁仓', warehouse: '简尚-睢宁仓主仓', dbWarehouse: '简尚-睢宁仓', wmsCoId: 15032603 },
{ idx: 21, group: '沭阳金源涌动', warehouse: '简尚-沭阳金源涌动项目仓主仓', dbWarehouse: '简尚-沭阳金源涌动项目仓', wmsCoId: 15043585 },
{ idx: 22, group: '分销仓', warehouse: '简尚-分销部睢宁专用仓主仓', dbWarehouse: '简尚-分销部睢宁专用仓', wmsCoId: 15051960 },
{ idx: 23, group: '外贸仓', warehouse: '简尚-新外贸仓主仓', dbWarehouse: '简尚-新外贸仓', wmsCoId: 15045308 },
{ idx: 24, group: '外贸仓', warehouse: '简尚-济南外贸仓主仓', dbWarehouse: null, wmsCoId: 15051888 },
{ idx: 25, group: '分销仓', warehouse: '简尚-分销部专用仓主仓', dbWarehouse: '简尚-分销部专用仓', wmsCoId: 15045426 },
{ idx: 26, group: '分销仓', warehouse: '简尚-分销部崔桥专用仓主仓', dbWarehouse: '简尚-分销部崔桥专用仓', wmsCoId: 15404402 },
];
function slugify(value) {
return String(value || '')
.toLowerCase()
.replace(/[^a-z0-9\u4e00-\u9fa5-]+/g, '-')
.replace(/^-+|-+$/g, '');
}
const LARGE_STOCK_MONITOR_WAREHOUSES = RAW_WAREHOUSES.map((entry) => ({
...entry,
routeKey: `stock-monitor-large-${entry.idx}`,
slug: slugify(entry.warehouse) || `warehouse-${entry.idx}`,
hasData: Boolean(entry.dbWarehouse),
}));
// 无业务库数据源的列:业务库(JSReportReplica,220 张表)中不存在加工/盘点/调拨/其它出库
// 任何单据表,这 6 列无真实数据可回填,保留占位符「—」并在前端说明。
const UNAVAILABLE_COLUMN_KEYS = [
'processIn',
'stocktake',
'processOut',
'transferOut',
'transferIn',
'otherOut',
];
const LARGE_STOCK_MONITOR_PAGE = {
pageKey: 'large_stock_monitor',
path: '/planning/large-stock-monitor/:warehouseSlug',
name: '大货库存监控',
defaultRoles: { admin: true, analyst: true, user: true, guest: false },
};
module.exports = {
LARGE_STOCK_MONITOR_WAREHOUSES,
LARGE_STOCK_MONITOR_PAGE,
UNAVAILABLE_COLUMN_KEYS,
};

View File

@ -35,7 +35,6 @@ const promotionModuleRoutes = require('./api/promotionModule');
const productMonitoringRoutes = require('./api/productMonitoring');
const modularStockMonitorRoutes = require('./api/modularStockMonitor');
const mkhBomRoutes = require('./api/mkhBom');
const largeStockMonitorRoutes = require('./api/largeStockMonitor');
const database = require('./config/database');
const cloudDb = require('./config/cloudDatabase');
const jianSunDb = require('./config/jianSunDatabase');
@ -655,7 +654,6 @@ async function startServer(options = {}) {
cubejsApp.use('/api/product-analysis', authMiddleware, productAnalysisRoutes);
cubejsApp.use('/api/marketing-events', authMiddleware, marketingEventsRoutes);
cubejsApp.use('/api/promotion-module', authMiddleware, promotionModuleRoutes);
cubejsApp.use('/api/large-stock-monitor', authMiddleware, largeStockMonitorRoutes);
cubejsApp.use('/api/planning-upload', authMiddleware, planningUploadPageCache, planningUploadRoutes);
cubejsApp.use('/api/channel', authMiddleware, channelPageCache, channelRoutes);
cubejsApp.use('/api/product-monitoring', authMiddleware, productMonitoringRoutes);

View File

@ -1,132 +0,0 @@
// 大货库存监控 - 出入库流水取数(本月 1 日 ~ 快照日)
//
// 业务库 JSReportReplica 里与「库存流水」相关的单据表:
// 采购进仓 rpt.jst_purchase (wms_co_id, po_date, status) + rpt.jst_purchaseItem (po_id, inQty)
// 采购退货 rpt.jst_purchaseout (wms_co_id, io_date, status) + rpt.jst_purchaseoutItem (io_id, qty)
// 销售出仓 rpt.jst_salesOutboundInfo(wms_co_id, io_date, status) + rpt.jst_OrderItem (o_id, qty)
// 销售退货 rpt.jst_returnAndRefundInfo(warehouse 名称) + rpt.jst_returnAndRefundItem(as_id, r_qty)
//
// 有效单据状态:只统计 Confirmed / Finished,排除 Cancelled / Delete / WaitConfirm。
// 业务库中不存在加工、盘点、调拨、其它出库任何单据表,这 6 列无法取数。
const cloudDb = require('../config/cloudDatabase');
// 单据有效状态(已确认/已完成),避免把取消单算进流水
const PURCHASE_STATUS = "'Confirmed','Finished'";
const PURCHASEOUT_STATUS = "'Confirmed'";
const SALESOUT_STATUS = "'Confirmed'";
const REFUND_STATUS = "'Confirmed'";
function toNumber(value) {
if (value == null) return null;
const n = Number(value);
return Number.isFinite(n) ? n : null;
}
/**
* 采购进仓:采购单已入库数量(inQty)
*/
async function fetchPurchaseIn(wmsCoId, periodStart, periodEnd) {
const rows = await cloudDb.query(
`SELECT ISNULL(SUM(CAST(pi.inQty AS DECIMAL(18, 2))), 0) AS v
FROM rpt.jst_purchase p
JOIN rpt.jst_purchaseItem pi ON pi.po_id = p.po_id
WHERE p.wms_co_id = @wmsCoId
AND p.po_date >= @periodStart AND p.po_date < @periodEnd
AND p.status IN (${PURCHASE_STATUS})`,
{ wmsCoId, periodStart, periodEnd }
);
return toNumber(rows[0]?.v);
}
/**
* 采购退货:采购退货出库单数量
*/
async function fetchPurchaseReturn(wmsCoId, periodStart, periodEnd) {
const rows = await cloudDb.query(
`SELECT ISNULL(SUM(CAST(oi.qty AS DECIMAL(18, 2))), 0) AS v
FROM rpt.jst_purchaseout o
JOIN rpt.jst_purchaseoutItem oi ON oi.io_id = o.io_id
WHERE o.wms_co_id = @wmsCoId
AND o.io_date >= @periodStart AND o.io_date < @periodEnd
AND o.status IN (${PURCHASEOUT_STATUS})`,
{ wmsCoId, periodStart, periodEnd }
);
return toNumber(rows[0]?.v);
}
/**
* 销售出仓:销售出库单关联的订单明细数量
* 注意:jst_salesOutboundInfo.qty 是表头字段且恒为 0,真实件数在 jst_OrderItem.qty
*/
async function fetchSalesOut(wmsCoId, periodStart, periodEnd) {
const rows = await cloudDb.query(
`SELECT ISNULL(SUM(CAST(oi.qty AS DECIMAL(18, 2))), 0) AS v
FROM rpt.jst_salesOutboundInfo s
JOIN rpt.jst_OrderItem oi ON oi.o_id = s.o_id
WHERE s.wms_co_id = @wmsCoId
AND s.io_date >= @periodStart AND s.io_date < @periodEnd
AND s.status IN (${SALESOUT_STATUS})`,
{ wmsCoId, periodStart, periodEnd }
);
return toNumber(rows[0]?.v);
}
/**
* 销售退货:退货退款单明细的实退数量(r_qty)
* 该表 wms_co_id 大量为空(9 月 92821 单中 75094 单为空),改用 warehouse 名称匹配:
* 名称形如「简尚-崔桥工厂仓销退仓」/「简尚-分销部睢宁专用仓主仓」,统一去掉「销退仓」「主仓」后比对。
*/
async function fetchSalesReturn(dbWarehouse, periodStart, periodEnd) {
if (!dbWarehouse) return null;
const rows = await cloudDb.query(
`SELECT ISNULL(SUM(CAST(ri.r_qty AS DECIMAL(18, 2))), 0) AS v
FROM rpt.jst_returnAndRefundInfo r
JOIN rpt.jst_returnAndRefundItem ri ON ri.as_id = r.as_id
WHERE REPLACE(REPLACE(ISNULL(r.warehouse, ''), N'销退仓', N''), N'主仓', N'') = @whName
AND r.created >= @periodStart AND r.created < @periodEnd
AND r.status IN (${REFUND_STATUS})`,
{ whName: dbWarehouse, periodStart, periodEnd }
);
return toNumber(rows[0]?.v);
}
/**
* 仓库实物库存:聚水潭实时库存表 rpt.jst_inventoryInfo(按 wms_co_id 单仓统计)
* 这是报表「期末数量」的取数口径 —— pm_stockMonitor 的 warehouse 仅是归属仓,
* 其 jsStock 含已调拨到外仓的货,不能代表本仓实物库存。
*/
async function fetchInventoryQty(wmsCoId) {
if (!wmsCoId) return null;
const rows = await cloudDb.query(
`SELECT ISNULL(SUM(CAST(qty AS DECIMAL(18, 2))), 0) AS v,
COUNT(CASE WHEN ISNULL(qty, 0) <> 0 THEN 1 END) AS skuCount,
MAX(modified) AS takenAt
FROM rpt.jst_inventoryInfo
WHERE wms_co_id = @wmsCoId`,
{ wmsCoId }
);
return {
qty: toNumber(rows[0]?.v),
skuCount: toNumber(rows[0]?.skuCount) || 0,
takenAt: rows[0]?.takenAt ? new Date(rows[0].takenAt).toISOString() : null,
};
}
/**
* 一次性取全部流水;任一子查询失败不影响其它字段(返回 null 由调用方降级为「—」)
*/
async function fetchFlowSummary({ wmsCoId, dbWarehouse, periodStart, periodEnd }) {
const [purchaseIn, purchaseReturn, salesOut, salesReturn] = await Promise.all([
wmsCoId ? fetchPurchaseIn(wmsCoId, periodStart, periodEnd).catch(() => null) : null,
wmsCoId ? fetchPurchaseReturn(wmsCoId, periodStart, periodEnd).catch(() => null) : null,
wmsCoId ? fetchSalesOut(wmsCoId, periodStart, periodEnd).catch(() => null) : null,
fetchSalesReturn(dbWarehouse, periodStart, periodEnd).catch(() => null),
]);
return { purchaseIn, purchaseReturn, salesOut, salesReturn };
}
module.exports = {
fetchFlowSummary,
fetchInventoryQty,
};

View File

@ -1,15 +0,0 @@
import request from './request';
const BASE = '/api/large-stock-monitor';
export function getLargeStockMonitorConfig() {
return request.get(`${BASE}/config`);
}
export function getLargeStockMonitorOverview(warehouse) {
return request.get(`${BASE}/overview`, { params: { warehouse } });
}
export function getLargeStockMonitorSkuList(params = {}) {
return request.get(`${BASE}/sku-list`, { params });
}

View File

@ -1,14 +1,3 @@
import { LARGE_STOCK_MONITOR_WAREHOUSES } from '../constants/largeStockMonitor.js';
const buildLargeStockMonitorChildren = () => LARGE_STOCK_MONITOR_WAREHOUSES.map((entry) => ({
key: `stock-monitor-large-${entry.idx}`,
label: entry.warehouse,
icon: 'appstore',
routeKey: `stock-monitor-large-${entry.idx}`,
visible: true,
order: entry.idx * 10,
}));
export const DEFAULT_SIDEBAR_CONFIG = {
collapsedByDefault: false,
darkModeByDefault: false,
@ -37,10 +26,6 @@ export const DEFAULT_SIDEBAR_CONFIG = {
{ key: 'mkh-bom-data', label: 'MKH-BOM资料', icon: 'table', routeKey: 'mkh-bom-data', visible: true, order: 20 },
],
},
{
key: 'stock-monitor-large', type: 'group', label: '大货库存监控', icon: 'appstore', visible: true, order: 200, defaultExpanded: false,
children: buildLargeStockMonitorChildren(),
},
{
key: 'manage', type: 'group', label: '系统管理', icon: 'setting', visible: true, order: 900, defaultExpanded: false, requiresAdmin: true,
children: [

View File

@ -1,165 +0,0 @@
// 大货库存监控配置(前端镜像)
// 与 backend/src/config/largeStockMonitorConfig.js 同步
// 修改需双端同步;运行期也可通过 /api/large-stock-monitor/config 获取
const RAW_WAREHOUSES = [
{ idx: 1, group: '半成品部件仓', warehouse: '简尚-崔桥工厂半成品包装项目仓主仓' },
{ idx: 2, group: '运营C', warehouse: '简尚-运营C崔桥工厂仓专用仓主仓' },
{ idx: 3, group: '崔桥工厂仓', warehouse: '简尚-崔桥工厂仓主仓' },
{ idx: 4, group: '崔桥工厂仓', warehouse: '简尚-崔桥工厂京东仓主仓' },
{ idx: 5, group: '崔桥工厂品牌仓', warehouse: '简尚-崔桥工厂品牌仓主仓' },
{ idx: 6, group: '河南洛宁仓', warehouse: '简尚-河南洛宁仓主仓' },
{ idx: 7, group: '运营D', warehouse: '简尚-运营D专用仓库主仓' },
{ idx: 8, group: '运营C', warehouse: '简尚-运营C横林二仓专用仓主仓' },
{ idx: 9, group: '横林二仓', warehouse: '简尚-横林二仓主仓' },
{ idx: 10, group: '横林三仓', warehouse: '简尚-横林三仓主仓' },
{ idx: 11, group: '山东品牌仓', warehouse: '简尚-山东品牌仓主仓' },
{ idx: 12, group: '山东青集岗仓', warehouse: '简尚-山东青岗集京东仓主仓' },
{ idx: 13, group: '山东青集岗仓', warehouse: '简尚-山东青岗集仓主仓' },
{ idx: 14, group: '运营D', warehouse: '简尚-运营D苏北专用仓主仓' },
{ idx: 15, group: '运营C', warehouse: '简尚-运营C新苏北专用仓主仓' },
{ idx: 16, group: '苏北仓', warehouse: '简尚-新苏北仓主仓' },
{ idx: 17, group: '分销仓', warehouse: '简尚-分销部苏北专用仓主仓' },
{ idx: 18, group: '运营D', warehouse: '简尚-运营D睢宁专用仓主仓' },
{ idx: 19, group: '运营C', warehouse: '简尚-运营C睢宁专用仓主仓' },
{ idx: 20, group: '睢宁仓', warehouse: '简尚-睢宁仓主仓' },
{ idx: 21, group: '沭阳金源涌动', warehouse: '简尚-沭阳金源涌动项目仓主仓' },
{ idx: 22, group: '分销仓', warehouse: '简尚-分销部睢宁专用仓主仓' },
{ idx: 23, group: '外贸仓', warehouse: '简尚-新外贸仓主仓' },
{ idx: 24, group: '外贸仓', warehouse: '简尚-济南外贸仓主仓' },
{ idx: 25, group: '分销仓', warehouse: '简尚-分销部专用仓主仓' },
{ idx: 26, group: '分销仓', warehouse: '简尚-分销部崔桥专用仓主仓' },
];
function slugify(value) {
return String(value || '')
.toLowerCase()
.replace(/[^a-z0-9\u4e00-\u9fa5-]+/g, '-')
.replace(/^-+|-+$/g, '');
}
export const LARGE_STOCK_MONITOR_WAREHOUSES = RAW_WAREHOUSES.map((entry) => ({
...entry,
routeKey: `stock-monitor-large-${entry.idx}`,
slug: slugify(entry.warehouse) || `warehouse-${entry.idx}`,
}));
// 18 列表头(按 Excel 第 1 行 D..U 列)
// available=false:业务库(220 张表)中不存在该单据类型,无数据源,显示「—」
// estimated=true:由已有字段推导(非原始字段),前端标「估」并在悬浮说明中写明推导式
export const OVERVIEW_COLUMNS = [
{
key: 'periodStart',
label: '期初数据',
available: true,
estimated: true,
tip: '业务库仅保留最新单日库存快照,无上月末历史。此值由「期末 − 采购进仓 + 采购退货 + 销售出仓 − 销售退货」倒推,未包含加工/调拨/盘点/其它出库。倒推为负时显示「—」。',
},
{ key: 'processIn', label: '加工进仓', available: false, tip: '业务库无加工单据表,无数据源' },
{
key: 'purchaseReturn',
label: '采购退货',
available: true,
tip: '本月采购退货出库单数量:rpt.jst_purchaseout + jst_purchaseoutItem(状态 Confirmed)',
},
{
key: 'purchaseIn',
label: '采购进仓',
available: true,
tip: '本月采购单已入库数量:rpt.jst_purchase + jst_purchaseItem 的 inQty(状态 Confirmed/Finished)',
},
{
key: 'salesOut',
label: '销售出仓',
available: true,
tip: '本月销售出库单件数:rpt.jst_salesOutboundInfo(状态 Confirmed)关联 jst_OrderItem.qty',
},
{ key: 'stocktake', label: '盘点', available: false, tip: '业务库无盘点单据表,无数据源' },
{ key: 'processOut', label: '加工出仓', available: false, tip: '业务库无加工单据表,无数据源' },
{ key: 'transferOut', label: '调拨出', available: false, tip: '业务库无调拨单据表,无数据源' },
{
key: 'salesReturn',
label: '销售退货',
available: true,
tip: '本月退货退款单实退数量:rpt.jst_returnAndRefundInfo + jst_returnAndRefundItem 的 r_qty(状态 Confirmed,按仓库名称匹配)',
},
{ key: 'transferIn', label: '调拨入', available: false, tip: '业务库无调拨单据表,无数据源' },
{
key: 'openingBalance',
label: '期初',
available: true,
estimated: true,
tip: '同期初数据(倒推估算值)',
},
{ key: 'otherOut', label: '其它出库', available: false, tip: '业务库无其它出库单据表,无数据源' },
{
key: 'closingQty',
label: '期末数量',
available: true,
tip: '聚水潭实际库存 rpt.jst_inventoryInfo(按仓库 wms_co_id 单仓实物口径,实时值)。业务库无历史时点库存快照,此值随每次刷新滚动到最新;与某日「期末结存」的差额即该日之后的出入库与仓间调拨',
},
{ key: 'turnoverDays', label: '库存周转天数', available: true, tip: '月度发货总件数 / 月度日均库存件数 × 100%(按业务库口径,数值实为周转率 %)' },
{ key: 'warnLevel', label: '滞销预警级库存量', available: true, tip: '库龄 < 3 个月 且 30% ≤ 周转率 ≤ 199% 的 SKU 库存合计(按库存快照分级,口径为归属库存,与期末实际库存不同)' },
{ key: 'slowLevel', label: '商品慢流级库存量', available: true, tip: '库龄 < 3 个月 且 周转率 ≥ 200%;或 库龄 > 3 个月 且 30% ≤ 周转率 ≤ 99%' },
{ key: 'staleLevel', label: '商品呆滞级库存量', available: true, tip: '库龄 3~12 个月 且 周转率 ≥ 100%;或 库龄 > 12 个月 且 100% ≤ 周转率 ≤ 199%' },
{ key: 'zombieLevel', label: '商品僵尸级库存量', available: true, tip: '库龄 > 12 个月 且 周转率 ≥ 200%' },
];
// 分级标签(与后端 levelExpr 返回值一致)
export const LEVEL_LABEL = {
warn: '滞销预警',
slow: '商品慢流',
stale: '商品呆滞',
zombie: '商品僵尸',
};
export const LEVEL_FILTER_OPTIONS = [
{ label: '全部级别', value: '' },
{ label: '滞销预警', value: 'warn' },
{ label: '商品慢流', value: 'slow' },
{ label: '商品呆滞', value: 'stale' },
{ label: '商品僵尸', value: 'zombie' },
];
export const LEVEL_COLOR = {
warn: 'gold',
slow: 'orange',
stale: 'volcano',
zombie: 'magenta',
};
export function findWarehouseBySlug(slug) {
return LARGE_STOCK_MONITOR_WAREHOUSES.find((entry) => entry.slug === slug) || null;
}
export function findWarehouseByName(name) {
return LARGE_STOCK_MONITOR_WAREHOUSES.find((entry) => entry.warehouse === name) || null;
}
export const PAGE_KEY = 'large_stock_monitor';
export const PAGE_PATH = '/planning/large-stock-monitor/:warehouseSlug';
export const PAGE_NAME = '大货库存监控';
// 顶部"算式说明"提示
export const FORMULA_DESCRIPTION = [
'库存周转天数 = 月度发货总件数 / 月度日均库存件数 × 100%(按业务库约定列名沿用「周转天数」,数值实际为周转率,单位 %)',
'月度发货总件数 ≈ threeAvgSale × 30 (业务库仅有最近 3 天日均销量)',
'月度日均库存件数 ≈ jsStock (业务库仅保留最近一天库存快照,作为日均近似)',
'数据截止时刻:库存快照日次日的凌晨 01:00(业务库每天凌晨 1 点跑批,快照标注为前一日 T-1)',
'库龄(月) = (数据截止时刻 - SKU 入库时间) / 30',
'分级口径:',
'· 滞销预警:库龄 < 3 个月 且 30% ≤ 周转天数 ≤ 199%',
'· 商品慢流:库龄 < 3 个月 且 周转天数 ≥ 200%;或 库龄 > 3 个月 且 30% ≤ 周转天数 ≤ 99%',
'· 商品呆滞:库龄 3~12 个月 且 周转天数 ≥ 100%;或 库龄 > 12 个月 且 100% ≤ 周转天数 ≤ 199%',
'· 商品僵尸:库龄 > 12 个月 且 周转天数 ≥ 200%',
'流水类字段(本期 = 快照所在自然月 1 日 00:00 ~ 数据截止时刻,左闭右开):',
'· 采购进仓:jst_purchase + jst_purchaseItem.inQty(Confirmed/Finished)',
'· 采购退货:jst_purchaseout + jst_purchaseoutItem.qty(Confirmed)',
'· 销售出仓:jst_salesOutboundInfo 关联 jst_OrderItem.qty(Confirmed)',
'· 销售退货:jst_returnAndRefundInfo + jst_returnAndRefundItem.r_qty(Confirmed,按仓库名匹配)',
'· 期初数据:业务库无上月末快照,由「期末 − 采购进仓 + 采购退货 + 销售出仓 − 销售退货」倒推(标「估」,倒推为负时显示「—」)',
'· 加工进仓 / 加工出仓 / 盘点 / 调拨出 / 调拨入 / 其它出库:业务库无对应单据表,无数据源',
'期末数量:聚水潭实际库存 jst_inventoryInfo(按 wms_co_id 单仓实物口径)',
'注意:pm_stockMonitor 的 warehouse 是「归属仓」,jsStock 含已调拨到外仓的货,不能代表本仓实物库存',
'四级库存量:只能由库存快照分级得出(实时库存表无入库时间与销量字段),口径为归属库存,与期末实际库存不同',
];

View File

@ -4,7 +4,6 @@ const DEFAULT_SYSTEM_ROLES = { admin: true, analyst: false, user: false, guest:
export const SIDEBAR_PAGES = [
{ pageKey: 'dashboard_preview', path: '/planning/dev-style-preview', name: '仪表盘', category: 'planning', defaultRoles: DEFAULT_PLANNING_ROLES },
{ pageKey: 'settings', path: '/settings', name: '系统设置', category: 'system', defaultRoles: DEFAULT_SYSTEM_ROLES },
{ pageKey: 'large_stock_monitor', path: '/planning/large-stock-monitor/:warehouseSlug', name: '大货库存监控', category: 'planning', defaultRoles: DEFAULT_PLANNING_ROLES },
];
export const SIDEBAR_PAGE_MAP = SIDEBAR_PAGES.reduce((acc, item) => {

View File

@ -36,7 +36,6 @@ import {
markDesktopEmbedSession,
requestDesktopSessionRenewal,
} from '../utils/desktopEmbedSession';
import { LARGE_STOCK_MONITOR_WAREHOUSES } from '../constants/largeStockMonitor.js';
const AUTH_USER_STORAGE_KEY = 'auth_user';
const SIDEBAR_DARK_MODE_STORAGE_KEY = 'maixiang_sidebar_dark_mode';
@ -98,14 +97,6 @@ const SIDEBAR_ROUTE_MAP = {
'desktop-execution-tasks': { path: '/settings/desktop-execution-tasks' },
};
const LARGE_STOCK_MONITOR_ROUTE_ENTRIES = LARGE_STOCK_MONITOR_WAREHOUSES.map((entry) => [
`stock-monitor-large-${entry.idx}`,
{ path: `/planning/large-stock-monitor/${entry.slug}`, matchMode: 'prefix' },
]);
LARGE_STOCK_MONITOR_ROUTE_ENTRIES.forEach(([key, value]) => {
SIDEBAR_ROUTE_MAP[key] = value;
});
const buildSidebarSections = (config, canUseSettings) => sortSidebarEntries(config.entries).flatMap((entry) => {
if (entry.requiresAdmin && !canUseSettings) return [];

View File

@ -1,502 +0,0 @@
import React, { useEffect, useMemo, useState } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import {
Alert,
Button,
Input,
Popover,
Segmented,
Skeleton,
Table,
Tag,
Tooltip,
} from 'antd';
import {
InfoCircleOutlined,
QuestionCircleOutlined,
ReloadOutlined,
SearchOutlined,
WarningOutlined,
} from '@ant-design/icons';
import { SycmBusinessPanel, SycmPage } from '../../components/Common';
import {
findWarehouseBySlug,
FORMULA_DESCRIPTION,
LARGE_STOCK_MONITOR_WAREHOUSES,
LEVEL_COLOR,
LEVEL_FILTER_OPTIONS,
LEVEL_LABEL,
OVERVIEW_COLUMNS,
} from '../../constants/largeStockMonitor';
import {
getLargeStockMonitorOverview,
getLargeStockMonitorSkuList,
} from '../../api/largeStockMonitor';
import './style.css';
function formatNumber(value) {
if (value === null || value === undefined || Number.isNaN(Number(value))) return value;
const n = Number(value);
if (!Number.isFinite(n)) return value;
return n.toLocaleString('zh-CN', { maximumFractionDigits: 2 });
}
function formatPercent(value) {
if (value === null || value === undefined || Number.isNaN(Number(value))) return value;
const n = Number(value);
if (!Number.isFinite(n)) return value;
return `${n.toLocaleString('zh-CN', { maximumFractionDigits: 1 })}%`;
}
function formatMonths(value) {
if (value === null || value === undefined || Number.isNaN(Number(value))) return value;
const n = Number(value);
if (!Number.isFinite(n)) return value;
return `${n.toLocaleString('zh-CN', { maximumFractionDigits: 1 })} 月`;
}
function formatStock(value) {
if (value === null || value === undefined || Number.isNaN(Number(value))) return value;
const n = Number(value);
if (!Number.isFinite(n)) return value;
return n.toLocaleString('zh-CN', { maximumFractionDigits: 0 });
}
const OVERVIEW_RENDERERS = {
periodStart: formatStock,
openingBalance: formatStock,
purchaseReturn: formatStock,
purchaseIn: formatStock,
salesOut: formatStock,
salesReturn: formatStock,
closingQty: formatStock,
turnoverDays: formatPercent,
warnLevel: formatStock,
slowLevel: formatStock,
staleLevel: formatStock,
zombieLevel: formatStock,
};
const FormulaHelp = () => (
<div className="large-stock-monitor-formula">
<div className="large-stock-monitor-formula__title">计算口径</div>
<ul>
{FORMULA_DESCRIPTION.map((line) => (
<li key={line}>{line}</li>
))}
</ul>
</div>
);
const LargeStockMonitor = () => {
const { warehouseSlug } = useParams();
const navigate = useNavigate();
const warehouse = useMemo(() => {
if (!warehouseSlug) return null;
return findWarehouseBySlug(warehouseSlug) || LARGE_STOCK_MONITOR_WAREHOUSES[0] || null;
}, [warehouseSlug]);
const [overview, setOverview] = useState(null);
const [overviewLoading, setOverviewLoading] = useState(true);
const [overviewError, setOverviewError] = useState(null);
const [skuListState, setSkuListState] = useState({
items: [],
total: 0,
page: 1,
pageSize: 20,
loading: false,
});
const [filterLevel, setFilterLevel] = useState('');
const [keyword, setKeyword] = useState('');
useEffect(() => {
if (!warehouse) {
setOverview(null);
setOverviewLoading(false);
return;
}
let cancelled = false;
setOverviewLoading(true);
setOverviewError(null);
getLargeStockMonitorOverview(warehouse.warehouse)
.then((res) => {
if (cancelled) return;
setOverview(res?.data || null);
})
.catch((error) => {
if (cancelled) return;
setOverviewError(error?.message || '获取概览失败');
setOverview(null);
})
.finally(() => {
if (cancelled) return;
setOverviewLoading(false);
});
return () => {
cancelled = true;
};
}, [warehouse]);
const reloadSkuList = (nextPage, nextPageSize, nextLevel, nextKeyword) => {
if (!warehouse) return;
setSkuListState((prev) => ({ ...prev, loading: true }));
getLargeStockMonitorSkuList({
warehouse: warehouse.warehouse,
page: nextPage,
pageSize: nextPageSize,
level: nextLevel,
keyword: nextKeyword,
})
.then((res) => {
setSkuListState({
items: res?.data?.items || [],
total: res?.data?.total || 0,
page: nextPage,
pageSize: nextPageSize,
loading: false,
});
})
.catch(() => {
setSkuListState((prev) => ({ ...prev, loading: false }));
});
};
useEffect(() => {
if (!warehouse) return;
reloadSkuList(1, skuListState.pageSize, filterLevel, keyword.trim());
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [warehouse, filterLevel]);
const handleTableChange = (pagination) => {
const nextPage = pagination.current || 1;
const nextPageSize = pagination.pageSize || 20;
reloadSkuList(nextPage, nextPageSize, filterLevel, keyword.trim());
};
const handleSearch = () => {
reloadSkuList(1, skuListState.pageSize, filterLevel, keyword.trim());
};
const overviewCells = useMemo(() => {
const map = overview?.overview || null;
return OVERVIEW_COLUMNS.map((column) => {
const value = map ? map[column.key] : '—';
const rendered = column.available && value !== '—' && OVERVIEW_RENDERERS[column.key]
? OVERVIEW_RENDERERS[column.key](value)
: value;
return { ...column, value, rendered };
});
}, [overview]);
const skuColumns = useMemo(() => ([
{
title: 'SKU',
dataIndex: 'sku',
key: 'sku',
width: 130,
fixed: 'left',
render: (value, record) => (
<div className="large-stock-monitor-sku-cell">
<strong>{value || '-'}</strong>
{record.spu ? <span className="large-stock-monitor-sku-cell__meta">SPU {record.spu}</span> : null}
</div>
),
},
{
title: '商品名称',
dataIndex: 'skuName',
key: 'skuName',
width: 260,
render: (value) => <span title={value}>{value || '-'}</span>,
},
{
title: '分类',
dataIndex: 'category',
key: 'category',
width: 120,
render: (value) => value || '-',
},
{
title: '供应商',
dataIndex: 'supplier',
key: 'supplier',
width: 140,
render: (value) => value || '-',
},
{
title: '当前库存',
dataIndex: 'jsStock',
key: 'jsStock',
width: 100,
align: 'right',
render: formatStock,
},
{
title: () => (
<span>
库龄
<Tooltip title="(今天 - 入库时间) / 30,单位:月">
<QuestionCircleOutlined style={{ marginLeft: 4, color: '#999' }} />
</Tooltip>
</span>
),
dataIndex: 'ageMonths',
key: 'ageMonths',
width: 100,
align: 'right',
render: formatMonths,
},
{
title: () => (
<span>
月度发货
<Tooltip title="≈ threeAvgSale × 30(业务库仅有 3 天日均销量)">
<QuestionCircleOutlined style={{ marginLeft: 4, color: '#999' }} />
</Tooltip>
</span>
),
dataIndex: 'monthlyShipment',
key: 'monthlyShipment',
width: 110,
align: 'right',
render: formatNumber,
},
{
title: () => (
<span>
日均库存
<Tooltip title="≈ jsStock(业务库仅保留最近一天快照,作为日均近似)">
<QuestionCircleOutlined style={{ marginLeft: 4, color: '#999' }} />
</Tooltip>
</span>
),
dataIndex: 'avgDailyStock',
key: 'avgDailyStock',
width: 110,
align: 'right',
render: formatStock,
},
{
title: () => (
<span>
库存周转天数
<Tooltip title="= 月度发货 / 日均库存 × 100%(实际为周转率 %,沿用 Excel 列名)">
<QuestionCircleOutlined style={{ marginLeft: 4, color: '#999' }} />
</Tooltip>
</span>
),
dataIndex: 'turnoverDays',
key: 'turnoverDays',
width: 130,
align: 'right',
render: formatPercent,
},
{
title: '级别',
dataIndex: 'level',
key: 'level',
width: 130,
fixed: 'right',
render: (value) => {
if (!value) return <span style={{ color: '#999' }}>—</span>;
return <Tag color={LEVEL_COLOR[value]}>{LEVEL_LABEL[value] || value}</Tag>;
},
},
]), []);
const siblingOptions = useMemo(() => LARGE_STOCK_MONITOR_WAREHOUSES.map((entry) => ({
label: `${entry.group} · ${entry.warehouse.replace(/^简尚-/, '')}`,
value: entry.slug,
idx: entry.idx,
group: entry.group,
})), []);
const fallbackNotice = warehouse && overview && !overview.hasData;
return (
<SycmPage className="large-stock-monitor-page">
<SycmBusinessPanel className="large-stock-monitor-header" aria-label="大货库存监控概览">
<div className="large-stock-monitor-header__top">
<div>
<h2>
大货库存监控 · {warehouse ? warehouse.warehouse : '请选择仓库'}
<Popover
content={<FormulaHelp />}
title="计算口径"
trigger="hover"
placement="rightTop"
>
<InfoCircleOutlined
style={{ marginLeft: 8, fontSize: 16, color: '#1677ff', cursor: 'help' }}
/>
</Popover>
</h2>
<p className="large-stock-monitor-header__meta">
<span>二级分类:{warehouse ? warehouse.group : '-'}</span>
<span>分布时间:{overview?.asOf || overview?.latestBizDate || '—'}</span>
{overview?.latestBizDate ? <span>库存快照日:{overview.latestBizDate}</span> : null}
{overview?.closingTakenAt ? (
<span>库存取数时间:{new Date(overview.closingTakenAt).toLocaleString('zh-CN', { hour12: false })}(实时)</span>
) : null}
{overview?.period ? (
<span>
流水区间:{overview.period.start} ~ {overview.period.end}(左闭右开)
</span>
) : null}
<span>SKU 数:{overview?.skuCount ?? '—'}</span>
<span>平均周转率:{overview?.turnoverDaysAvg != null ? formatPercent(overview.turnoverDaysAvg) : '—'}</span>
<span>最大周转率:{overview?.turnoverDaysMax != null ? formatPercent(overview.turnoverDaysMax) : '—'}</span>
</p>
</div>
<div className="large-stock-monitor-header__actions">
<Segmented
value={warehouse?.slug}
onChange={(value) => {
if (value) {
navigate(`/planning/large-stock-monitor/${value}`);
}
}}
options={siblingOptions.slice(0, 6)}
className="large-stock-monitor-quick-switch"
/>
<Button
icon={<ReloadOutlined />}
onClick={() => {
if (warehouse) navigate(`/planning/large-stock-monitor/${warehouse.slug}`);
}}
>
刷新
</Button>
</div>
</div>
{overviewError ? (
<Alert
type="error"
showIcon
message={`获取概览失败:${overviewError}`}
style={{ marginBottom: 12 }}
/>
) : null}
{fallbackNotice ? (
<Alert
type="warning"
showIcon
icon={<WarningOutlined />}
message="业务库暂未同步该仓库的库存快照,周转率与四级库存量无数据源,显示「—」"
style={{ marginBottom: 12 }}
/>
) : null}
{overview && overview.hasGrade === false ? (
<Alert
type="info"
showIcon
message="该仓库没有库存快照(pm_stockMonitor),库龄、周转天数与四级库存量无数据源,显示「—」;期末数量已按聚水潭实际库存取数"
style={{ marginBottom: 12 }}
/>
) : null}
{overview?.hasGrade && overview?.gradeBasisStock ? (
<Alert
type="warning"
showIcon
icon={<WarningOutlined />}
message={`期末数量按聚水潭实际库存统计;四级库存量由库存快照分级得出,快照为「归属仓」口径(含已调拨到外仓的货,合计 ${formatStock(overview.gradeBasisStock)}),两者口径不同,不能直接相加核对`}
style={{ marginBottom: 12 }}
/>
) : null}
{overview?.requestedDate && overview?.fallbackDate ? (
<Alert
type="info"
showIcon
message={`未找到 ${overview.requestedDate} 的库存快照,已自动回退并展示 ${overview.fallbackDate} 的数据`}
style={{ marginBottom: 12 }}
/>
) : null}
{overview?.requestedDate && !overview?.fallbackDate && !overview?.hasData ? (
<Alert
type="warning"
showIcon
message={`未找到 ${overview.requestedDate} 及之前的库存快照,当前汇总字段全部为占位符「—」`}
style={{ marginBottom: 12 }}
/>
) : null}
<div className="large-stock-monitor-overview">
{overviewLoading ? (
<Skeleton active paragraph={{ rows: 2 }} />
) : (
<div className="large-stock-monitor-grid">
{overviewCells.map((cell) => (
<div
key={cell.key}
className={`large-stock-monitor-grid__cell${cell.available ? ' is-available' : ' is-placeholder'}`}
>
<span className="large-stock-monitor-grid__label">
{cell.label}
{cell.estimated ? <em className="large-stock-monitor-grid__est">估</em> : null}
{cell.tip ? (
<Tooltip title={cell.tip}>
<QuestionCircleOutlined className="large-stock-monitor-grid__help" />
</Tooltip>
) : null}
</span>
<strong className="large-stock-monitor-grid__value">{cell.rendered}</strong>
</div>
))}
</div>
)}
</div>
</SycmBusinessPanel>
<SycmBusinessPanel className="large-stock-monitor-table" aria-label="SKU 库存明细">
<div className="large-stock-monitor-toolbar">
<span className="large-stock-monitor-toolbar__note">
明细按库存快照(归属仓口径)分级,用于识别滞销/慢流/呆滞/僵尸 SKU
</span>
<Segmented
value={filterLevel}
onChange={setFilterLevel}
options={LEVEL_FILTER_OPTIONS}
/>
<Input.Search
allowClear
placeholder="搜索 SKU 编码或商品名称"
value={keyword}
onChange={(event) => setKeyword(event.target.value)}
onSearch={handleSearch}
enterButton={<><SearchOutlined /> 查询</>}
style={{ maxWidth: 320 }}
/>
</div>
<Table
className="large-stock-monitor-data-table"
rowKey={(record) => `${record.sku}-${record.warehouse}`}
columns={skuColumns}
dataSource={skuListState.items}
loading={skuListState.loading}
scroll={{ x: 1280 }}
pagination={{
current: skuListState.page,
pageSize: skuListState.pageSize,
total: skuListState.total,
showSizeChanger: true,
showTotal: (total) => `共 ${total} 条`,
pageSizeOptions: ['20', '50', '100'],
}}
onChange={handleTableChange}
locale={{ emptyText: '该仓库暂无 SKU 库存快照' }}
/>
</SycmBusinessPanel>
</SycmPage>
);
};
export default LargeStockMonitor;

View File

@ -1,149 +0,0 @@
.large-stock-monitor-page {
display: flex;
flex-direction: column;
gap: var(--sycm-space-md);
padding: var(--sycm-space-md);
}
.large-stock-monitor-header__top {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
flex-wrap: wrap;
}
.large-stock-monitor-header__top h2 {
margin: 0 0 4px;
font-size: var(--sycm-font-size-title);
font-weight: var(--sycm-font-weight-semibold);
}
.large-stock-monitor-header__meta {
display: flex;
flex-wrap: wrap;
gap: 12px;
margin: 0;
color: var(--sycm-text-secondary);
font-size: var(--sycm-font-size-caption);
}
.large-stock-monitor-header__actions {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.large-stock-monitor-quick-switch {
max-width: 480px;
}
.large-stock-monitor-overview {
margin-top: 12px;
}
.large-stock-monitor-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
gap: 12px;
}
.large-stock-monitor-grid__cell {
display: flex;
flex-direction: column;
gap: 4px;
padding: 12px;
border-radius: var(--sycm-radius-sm);
background: var(--sycm-bg-subtle);
border: 1px solid transparent;
}
.large-stock-monitor-grid__cell.is-available {
background: var(--sycm-bg-elevated, #fff);
border-color: var(--sycm-color-primary-border);
}
.large-stock-monitor-grid__cell.is-placeholder {
color: var(--sycm-text-tertiary);
}
.large-stock-monitor-grid__label {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: var(--sycm-font-size-caption);
font-weight: var(--sycm-font-weight-medium);
color: var(--sycm-text-secondary);
}
/* 估算值标记:由已有字段推导,非业务库原始字段 */
.large-stock-monitor-grid__est {
padding: 0 4px;
border-radius: 3px;
background: var(--sycm-color-warning-bg, #fffbe6);
border: 1px solid var(--sycm-color-warning-border, #ffe58f);
color: var(--sycm-color-warning-text, #d46b08);
font-size: var(--sycm-font-size-micro);
font-style: normal;
line-height: 16px;
}
.large-stock-monitor-grid__help {
color: var(--sycm-text-tertiary);
cursor: help;
font-size: var(--sycm-font-size-caption);
}
.large-stock-monitor-grid__value {
font-size: var(--sycm-font-size-title);
font-weight: var(--sycm-font-weight-semibold);
color: var(--sycm-text-primary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.large-stock-monitor-toolbar {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-bottom: 12px;
}
.large-stock-monitor-toolbar__note {
font-size: 12px;
color: #8c8c8c;
line-height: 1.5;
}
.large-stock-monitor-data-table .large-stock-monitor-sku-cell {
display: flex;
flex-direction: column;
line-height: 1.3;
}
.large-stock-monitor-data-table .large-stock-monitor-sku-cell__meta {
color: var(--sycm-text-tertiary);
font-size: var(--sycm-font-size-micro);
}
.large-stock-monitor-formula {
max-width: 480px;
font-size: var(--sycm-font-size-caption);
color: var(--sycm-text-secondary);
}
.large-stock-monitor-formula__title {
font-weight: var(--sycm-font-weight-semibold);
color: var(--sycm-text-primary);
margin-bottom: 8px;
}
.large-stock-monitor-formula ul {
margin: 0;
padding-left: 18px;
line-height: 1.7;
}

View File

@ -19,7 +19,6 @@ const OpsBIncentiveBoard = lazy(() => import('../pages/PlanningUpload/OpsBIncent
const ProductMonitoring = lazy(() => import('../pages/ProductMonitoring'));
const ModularStockMonitor = lazy(() => import('../pages/ModularStockMonitor'));
const MkhBomData = lazy(() => import('../pages/MkhBomData'));
const LargeStockMonitor = lazy(() => import('../pages/LargeStockMonitor'));
const lazyLoadingElement = <PageLoadingFallback />;
@ -132,10 +131,6 @@ const router = createBrowserRouter([
path: 'planning/dev-style-preview',
element: renderLazyComponent(DevelopmentStylePreview),
},
{
path: 'planning/large-stock-monitor/:warehouseSlug',
element: renderLazyComponent(LargeStockMonitor),
},
{
path: 'settings',
element: renderLazyComponent(Settings),