cangku/frontend/src/config/sidebarNavigation.js
yanhui 6a5f6e8ae0 refactor(wst): 页面名去掉「24 小时」,改为「发货时效(到点未发即超时)」
规则完全不动,只改名字,消除误导。

## 为什么改

页面原名「仓储中心 24 小时发货时效」,但判定规则是「到 13:00 / 18:00
核验时刻仍未发货即超时」,**没有满 24 小时的门槛**(业务 2026-10-20 已三次纠偏定版)。
名字和规则打架,导致看的人以为「不满 24 小时不该算超时」。

实测例子:10-27 16:00 创建的单,今天 13:00 核验时只等了 **21 小时**就被判超时;
13:00 轮 599 单里有 416 单属于 10-27 16 点后创建。业务看到就问「创建时间都在 7 点后,
为什么算超时」—— 其实是名字里的「24 小时」在误导(那些是**昨天**晚上的单,
不是今天的)。

## 改了什么

用户可见(2 处):
- 侧栏菜单:`仓储中心24小时发货时效` → `仓储中心发货时效`
  (不加括号:侧栏现有最长 label 12 字,加了会到 18 字溢出)
- 页面标题:`仓储中心 24 小时发货时效` → `仓储中心发货时效(到点未发即超时)`

其余为注释同步(8 个文件),代码逻辑与判定口径**零改动**:
`warehouseShipTimelinessRegistry/Service/NotifyJob/NotifySettings`、
`warehouseLogisticsExceptionRegistry`、`frontend/api/warehouseShipTimeliness`、`backend/index`。

## 不变的部分

- 核验时点、窗口上界(07:00 / 16:00)、仓级例外(山东 15:00、洛宁 13:00)
- 数据源与起算点(订单两表 + `created`)
- 钉钉推送抬头本来就是「【仓储中心发货时效】」,不用改

## 验证

- 前端 eslint 通过、`npm run build` 通过(31.86s)
- 浏览器真渲染(本地 5173,等足 75 秒数据返回):
  - 标题 = `仓储中心发货时效(到点未发即超时)`
  - 侧栏项 = `仓储中心发货时效`,全页 `body` 内已无「24小时」字样
  - 数据正常渲染:超时未发 599 单 / 不计考核 849 / 涉及仓库 8 个 / 最久未发 13 天
  - 明细「创建时间」列完整显示 `2026-10-27 19:xx` 这类日期,不存在时区错位
  - 无 pageerror
- 后端 `npm run check`:本次只动注释,不影响
2026-10-28 14:30:54 +08:00

217 lines
10 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

export const DEFAULT_SIDEBAR_CONFIG = {
collapsedByDefault: false,
darkModeByDefault: false,
entries: [
{ key: 'product-monitoring', type: 'item', label: '商品监控', icon: 'appstore', routeKey: 'product-monitoring', visible: true, order: 12 },
{ key: 'ops-b-incentive', type: 'item', label: '激励看板', icon: 'line-chart', routeKey: 'ops-b-incentive', visible: true, order: 15 },
{ key: 'dashboard-preview', type: 'item', label: '仪表盘', icon: 'dashboard', routeKey: 'dashboard-preview', visible: true, order: 50 },
{
key: 'marketing-management', type: 'group', label: '营销管理', icon: 'shopping', visible: true, order: 60, defaultExpanded: true,
children: [
{ key: 'marketing-events', label: '营销活动', icon: 'projection', routeKey: 'marketing-events', visible: true, order: 30 },
{ key: 'promotion-module', label: '推广模块', icon: 'line-chart', routeKey: 'promotion-module', visible: true, order: 50 },
],
},
{
key: 'product-management', type: 'group', label: '商品管理', icon: 'appstore', visible: true, order: 70, defaultExpanded: true,
children: [
{ key: 'product-analysis', label: '商品分析', icon: 'table', routeKey: 'product-analysis', visible: true, order: 20 },
],
},
{
key: 'returns-inventory', type: 'group', label: '退货库存监控', icon: 'inbox', visible: true, order: 75, defaultExpanded: true,
children: [
{ key: 'returns-inventory-cuiqiao', label: '崔桥工厂仓客退数据监控', icon: 'table', routeKey: 'returns-inventory-cuiqiao', visible: true, order: 10 },
{ key: 'returns-inventory-subei', label: '苏北客退数据监控', icon: 'table', routeKey: 'returns-inventory-subei', visible: true, order: 20 },
{ key: 'returns-inventory-suining', label: '睢宁客退数据监控', icon: 'table', routeKey: 'returns-inventory-suining', visible: true, order: 30 },
{ key: 'returns-inventory-shandong', label: '山东客退数据监控', icon: 'table', routeKey: 'returns-inventory-shandong', visible: true, order: 40 },
{ key: 'returns-inventory-henan', label: '河南客退数据监控', icon: 'table', routeKey: 'returns-inventory-henan', visible: true, order: 50 },
],
},
{ key: 'work-orders', type: 'item', label: '工单待办', icon: 'message', routeKey: 'work-orders', visible: true, order: 100 },
{ key: 'bin-replenish-monitor', type: 'item', label: '缺货补货监控', icon: 'inbox', routeKey: 'bin-replenish-monitor', visible: true, order: 90 },
{
key: 'modular-stock-monitor', type: 'group', label: '模块化库存监控', icon: 'appstore', visible: true, order: 105, defaultExpanded: true,
children: [
{ key: 'modular-stock-monitor-accessory', label: '配件库存监控', icon: 'table', routeKey: 'modular-stock-monitor-accessory', visible: true, order: 10 },
{ key: 'mkh-bom-data', label: 'MKH-BOM资料', icon: 'table', routeKey: 'mkh-bom-data', visible: true, order: 20 },
],
},
{ key: 'warehouse-ship-timeliness', type: 'item', label: '仓储中心发货时效', icon: 'inbox', routeKey: 'warehouse-ship-timeliness', visible: true, order: 110 },
{ key: 'warehouse-logistics-exception', type: 'item', label: '仓储中心物流转运异常', icon: 'warning', routeKey: 'warehouse-logistics-exception', visible: true, order: 111 },
{
key: 'large-stock-monitor', type: 'group', label: '大货库存监控', icon: 'appstore', visible: true, order: 200, defaultExpanded: true,
children: [
{ key: 'daily-inventory', label: '每日进销存', icon: 'table', routeKey: 'daily-inventory', visible: true, order: 10 },
{ key: 'stock-sales-analysis', label: '库存动销分析', icon: 'line-chart', routeKey: 'stock-sales-analysis', visible: true, order: 20 },
{ key: 'picking-bin-sales-analysis', label: '拣选库位动销分析', icon: 'inbox', routeKey: 'picking-bin-sales-analysis', visible: true, order: 30 },
],
},
{
key: 'manage', type: 'group', label: '系统管理', icon: 'setting', visible: true, order: 900, defaultExpanded: false, requiresAdmin: true,
children: [
{ key: 'settings', label: '系统设置', icon: 'setting', routeKey: 'settings', visible: true, order: 10, requiresAdmin: true },
{ key: 'online-desktop-users', label: '在线客户端', icon: 'team', routeKey: 'online-desktop-users', visible: true, order: 30, requiresAdmin: true },
{ key: 'desktop-execution-tasks', label: '执行任务', icon: 'projection', routeKey: 'desktop-execution-tasks', visible: true, order: 40, requiresAdmin: true },
],
},
],
};
const cloneDefaultConfig = () => JSON.parse(JSON.stringify(DEFAULT_SIDEBAR_CONFIG));
const flattenEntryKeys = (entries = []) => entries.flatMap((entry) => [
entry.key,
...flattenEntryKeys(entry.children || []),
]).filter(Boolean);
const flattenRouteKeys = (entries = []) => entries.flatMap((entry) => [
entry.routeKey,
...flattenRouteKeys(entry.children || []),
]).filter(Boolean);
const ALLOWED_ENTRY_KEYS = new Set(flattenEntryKeys(DEFAULT_SIDEBAR_CONFIG.entries));
const ALLOWED_ROUTE_KEYS = new Set(flattenRouteKeys(DEFAULT_SIDEBAR_CONFIG.entries));
// 激励看板原为「运营工具 → 运营中心B激励看板」,现升级为顶层入口;用于迁移存量侧边栏配置
const OPS_B_INCENTIVE_LEGACY_LABEL = '运营中心B激励看板';
const OPS_B_INCENTIVE_LEGACY_ORDER = 6;
const PROMOTED_TOP_LEVEL_ENTRY_KEYS = new Set(['ops-b-incentive']);
// 把存量配置里仍嵌在分组下的已升级条目摘出来,提升为顶层配置参与合并
const extractPromotedTopLevelEntries = (entries = []) => {
const promoted = [];
const nextEntries = entries.map((entry) => {
if (!Array.isArray(entry?.children)) return entry;
const children = [];
entry.children.forEach((child) => {
if (child?.key && PROMOTED_TOP_LEVEL_ENTRY_KEYS.has(child.key)) {
promoted.push(child);
} else {
children.push(child);
}
});
return children.length === entry.children.length ? entry : { ...entry, children };
});
return { nextEntries, promoted };
};
const mergeSidebarEntries = (defaultEntries = [], configuredEntries = []) => {
const defaultByKey = new Map(defaultEntries.map((entry) => [entry?.key, entry]).filter(([key]) => key));
const configuredKeys = new Set();
const mergeEntry = (defaultEntry = {}, configuredEntry = {}) => {
const merged = { ...defaultEntry, ...configuredEntry };
if (defaultEntry.type) {
merged.type = defaultEntry.type;
}
if (defaultEntry.routeKey) {
merged.routeKey = defaultEntry.routeKey;
}
if (typeof defaultEntry.requiresAdmin !== 'undefined') {
merged.requiresAdmin = defaultEntry.requiresAdmin;
}
if (
defaultEntry.key === 'ops-b-incentive'
&& (!configuredEntry.label || configuredEntry.label === OPS_B_INCENTIVE_LEGACY_LABEL)
) {
merged.label = defaultEntry.label;
}
if (
defaultEntry.key === 'ops-b-incentive'
&& (configuredEntry.order == null || configuredEntry.order === OPS_B_INCENTIVE_LEGACY_ORDER)
) {
merged.order = defaultEntry.order;
}
const hasDefaultChildren = Array.isArray(defaultEntry.children);
const hasConfiguredChildren = Array.isArray(configuredEntry.children);
if (hasDefaultChildren || hasConfiguredChildren) {
if (hasDefaultChildren) {
merged.type = defaultEntry.type;
} else if (defaultEntry.routeKey) {
merged.type = defaultEntry.type || 'item';
delete merged.children;
delete merged.defaultExpanded;
return merged;
}
merged.children = mergeSidebarEntries(defaultEntry.children || [], configuredEntry.children || []);
}
return merged;
};
const configuredMerged = configuredEntries.map((entry) => {
if (entry?.key) {
configuredKeys.add(entry.key);
}
const defaultEntry = defaultByKey.get(entry?.key);
return defaultEntry ? mergeEntry(defaultEntry, entry) : entry;
});
const missingDefaults = defaultEntries
.filter((entry) => entry?.key && !configuredKeys.has(entry.key))
.map((entry) => mergeEntry(entry, {}));
return [...configuredMerged, ...missingDefaults];
};
const mergeSidebarConfigWithDefaults = (config) => {
const defaults = cloneDefaultConfig();
const nestedEntries = (config?.entries || []);
const { nextEntries, promoted } = extractPromotedTopLevelEntries(nestedEntries);
const topLevelKeys = new Set(nextEntries.map((entry) => entry?.key).filter(Boolean));
const promotedNotDuplicated = promoted.some((entry) => topLevelKeys.has(entry.key)) ? [] : promoted;
return {
...defaults,
...config,
entries: mergeSidebarEntries(defaults.entries, [...nextEntries, ...promotedNotDuplicated]),
};
};
const pruneSidebarEntry = (entry) => {
if (!entry || !ALLOWED_ENTRY_KEYS.has(entry.key)) {
return null;
}
const children = Array.isArray(entry.children)
? entry.children.map(pruneSidebarEntry).filter(Boolean)
: undefined;
const hasAllowedRoute = entry.routeKey ? ALLOWED_ROUTE_KEYS.has(entry.routeKey) : false;
const hasAllowedChildren = Array.isArray(children) && children.length > 0;
const keepEntry = hasAllowedRoute || hasAllowedChildren;
if (!keepEntry) {
return null;
}
return {
...entry,
...(Array.isArray(entry.children) ? { children } : {}),
};
};
const removeRetiredEntries = (config) => ({
...config,
entries: (config.entries || []).map(pruneSidebarEntry).filter(Boolean),
});
export const parseSidebarConfig = (value) => {
if (!value) return cloneDefaultConfig();
try {
const parsed = typeof value === 'string' ? JSON.parse(value) : value;
if (!parsed || !Array.isArray(parsed.entries)) return cloneDefaultConfig();
return removeRetiredEntries(mergeSidebarConfigWithDefaults(parsed));
} catch {
return cloneDefaultConfig();
}
};
export const sortSidebarEntries = (entries = []) => (
[...entries]
.filter((entry) => entry?.visible !== false)
.sort((a, b) => Number(a.order || 0) - Number(b.order || 0))
);