diff --git a/backend/scripts/verify-large-stock-monitor.js b/backend/scripts/verify-large-stock-monitor.js deleted file mode 100644 index d1ca3a2..0000000 --- a/backend/scripts/verify-large-stock-monitor.js +++ /dev/null @@ -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); -})(); diff --git a/backend/src/api/largeStockMonitor.js b/backend/src/api/largeStockMonitor.js deleted file mode 100644 index ecea5a5..0000000 --- a/backend/src/api/largeStockMonitor.js +++ /dev/null @@ -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; diff --git a/backend/src/config/largeStockMonitorConfig.js b/backend/src/config/largeStockMonitorConfig.js deleted file mode 100644 index 4d8de40..0000000 --- a/backend/src/config/largeStockMonitorConfig.js +++ /dev/null @@ -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, -}; diff --git a/backend/src/index.js b/backend/src/index.js index 4b0007d..88daa64 100644 --- a/backend/src/index.js +++ b/backend/src/index.js @@ -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); diff --git a/backend/src/services/largeStockMonitorFlow.js b/backend/src/services/largeStockMonitorFlow.js deleted file mode 100644 index 200cf5f..0000000 --- a/backend/src/services/largeStockMonitorFlow.js +++ /dev/null @@ -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, -}; diff --git a/frontend/src/api/largeStockMonitor.js b/frontend/src/api/largeStockMonitor.js deleted file mode 100644 index 084edab..0000000 --- a/frontend/src/api/largeStockMonitor.js +++ /dev/null @@ -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 }); -} diff --git a/frontend/src/config/sidebarNavigation.js b/frontend/src/config/sidebarNavigation.js index d99a7b4..63a6520 100644 --- a/frontend/src/config/sidebarNavigation.js +++ b/frontend/src/config/sidebarNavigation.js @@ -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: [ diff --git a/frontend/src/constants/largeStockMonitor.js b/frontend/src/constants/largeStockMonitor.js deleted file mode 100644 index 38392f7..0000000 --- a/frontend/src/constants/largeStockMonitor.js +++ /dev/null @@ -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 含已调拨到外仓的货,不能代表本仓实物库存', - '四级库存量:只能由库存快照分级得出(实时库存表无入库时间与销量字段),口径为归属库存,与期末实际库存不同', -]; \ No newline at end of file diff --git a/frontend/src/constants/sidebarPages.js b/frontend/src/constants/sidebarPages.js index 6c12aef..4545a75 100644 --- a/frontend/src/constants/sidebarPages.js +++ b/frontend/src/constants/sidebarPages.js @@ -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) => { diff --git a/frontend/src/layouts/BasicLayout.jsx b/frontend/src/layouts/BasicLayout.jsx index 8d60f31..6cb7ddc 100644 --- a/frontend/src/layouts/BasicLayout.jsx +++ b/frontend/src/layouts/BasicLayout.jsx @@ -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 []; diff --git a/frontend/src/pages/LargeStockMonitor/index.jsx b/frontend/src/pages/LargeStockMonitor/index.jsx deleted file mode 100644 index 171601a..0000000 --- a/frontend/src/pages/LargeStockMonitor/index.jsx +++ /dev/null @@ -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 = () => ( -
-
计算口径
- -
-); - -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) => ( -
- {value || '-'} - {record.spu ? SPU {record.spu} : null} -
- ), - }, - { - title: '商品名称', - dataIndex: 'skuName', - key: 'skuName', - width: 260, - render: (value) => {value || '-'}, - }, - { - 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: () => ( - - 库龄 - - - - - ), - dataIndex: 'ageMonths', - key: 'ageMonths', - width: 100, - align: 'right', - render: formatMonths, - }, - { - title: () => ( - - 月度发货 - - - - - ), - dataIndex: 'monthlyShipment', - key: 'monthlyShipment', - width: 110, - align: 'right', - render: formatNumber, - }, - { - title: () => ( - - 日均库存 - - - - - ), - dataIndex: 'avgDailyStock', - key: 'avgDailyStock', - width: 110, - align: 'right', - render: formatStock, - }, - { - title: () => ( - - 库存周转天数 - - - - - ), - dataIndex: 'turnoverDays', - key: 'turnoverDays', - width: 130, - align: 'right', - render: formatPercent, - }, - { - title: '级别', - dataIndex: 'level', - key: 'level', - width: 130, - fixed: 'right', - render: (value) => { - if (!value) return —; - return {LEVEL_LABEL[value] || value}; - }, - }, - ]), []); - - 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 ( - - -
-
-

- 大货库存监控 · {warehouse ? warehouse.warehouse : '请选择仓库'} - } - title="计算口径" - trigger="hover" - placement="rightTop" - > - - -

-

- 二级分类:{warehouse ? warehouse.group : '-'} - 分布时间:{overview?.asOf || overview?.latestBizDate || '—'} - {overview?.latestBizDate ? 库存快照日:{overview.latestBizDate} : null} - {overview?.closingTakenAt ? ( - 库存取数时间:{new Date(overview.closingTakenAt).toLocaleString('zh-CN', { hour12: false })}(实时) - ) : null} - {overview?.period ? ( - - 流水区间:{overview.period.start} ~ {overview.period.end}(左闭右开) - - ) : null} - SKU 数:{overview?.skuCount ?? '—'} - 平均周转率:{overview?.turnoverDaysAvg != null ? formatPercent(overview.turnoverDaysAvg) : '—'} - 最大周转率:{overview?.turnoverDaysMax != null ? formatPercent(overview.turnoverDaysMax) : '—'} -

-
-
- { - if (value) { - navigate(`/planning/large-stock-monitor/${value}`); - } - }} - options={siblingOptions.slice(0, 6)} - className="large-stock-monitor-quick-switch" - /> - -
-
- - {overviewError ? ( - - ) : null} - - {fallbackNotice ? ( - } - message="业务库暂未同步该仓库的库存快照,周转率与四级库存量无数据源,显示「—」" - style={{ marginBottom: 12 }} - /> - ) : null} - - {overview && overview.hasGrade === false ? ( - - ) : null} - - {overview?.hasGrade && overview?.gradeBasisStock ? ( - } - message={`期末数量按聚水潭实际库存统计;四级库存量由库存快照分级得出,快照为「归属仓」口径(含已调拨到外仓的货,合计 ${formatStock(overview.gradeBasisStock)}),两者口径不同,不能直接相加核对`} - style={{ marginBottom: 12 }} - /> - ) : null} - - {overview?.requestedDate && overview?.fallbackDate ? ( - - ) : null} - - {overview?.requestedDate && !overview?.fallbackDate && !overview?.hasData ? ( - - ) : null} - -
- {overviewLoading ? ( - - ) : ( -
- {overviewCells.map((cell) => ( -
- - {cell.label} - {cell.estimated ? 估 : null} - {cell.tip ? ( - - - - ) : null} - - {cell.rendered} -
- ))} -
- )} -
-
- - -
- - 明细按库存快照(归属仓口径)分级,用于识别滞销/慢流/呆滞/僵尸 SKU - - - setKeyword(event.target.value)} - onSearch={handleSearch} - enterButton={<> 查询} - style={{ maxWidth: 320 }} - /> -
- - `${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 库存快照' }} - /> - - - ); -}; - -export default LargeStockMonitor; \ No newline at end of file diff --git a/frontend/src/pages/LargeStockMonitor/style.css b/frontend/src/pages/LargeStockMonitor/style.css deleted file mode 100644 index 535141b..0000000 --- a/frontend/src/pages/LargeStockMonitor/style.css +++ /dev/null @@ -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; -} diff --git a/frontend/src/router/index.jsx b/frontend/src/router/index.jsx index 78cd981..0c64a0c 100644 --- a/frontend/src/router/index.jsx +++ b/frontend/src/router/index.jsx @@ -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 = ; @@ -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),