- 将 ShipOrderList 组件从 Delivery 模块迁移至 Order 模块 - 移除 Delivery 模块的导出并在 Order 模块中新增 OrderShipList 和相关组件 - 更新发货单列表的列配置,添加订单、经销商、公司等关联字段 - 修改发货单列表的字段映射,将经销商名称和车次号格式从短横线改为竖线分隔 - 在 BizPage 组件中添加 convertValue 属性支持数据转换 - 更新左侧菜单组件的依赖数组以包含 menuData - 调整发货单列表的国际化配置,更新字段标题和状态枚举值 - 新增 OrderShip 页面路由文件 - 移除项目根目录的 AGENTS.md 和 project.md 文件 - 从组件索引中移除 Delivery 模块并添加 SearchMenu 组件
268 lines
6.4 KiB
TypeScript
268 lines
6.4 KiB
TypeScript
import {
|
|
ClockCircleOutlined,
|
|
SearchOutlined,
|
|
StarOutlined,
|
|
} from '@ant-design/icons';
|
|
import { MenuDataItem } from '@ant-design/pro-components';
|
|
import { history } from '@umijs/max';
|
|
import type { AutoCompleteProps } from 'antd';
|
|
import { AutoComplete, Input } from 'antd';
|
|
import React, { useMemo, useState } from 'react';
|
|
import useSearchMenuStyle from './style.style';
|
|
|
|
interface SearchMenuProps {
|
|
menuData?: MenuDataItem[];
|
|
}
|
|
|
|
interface FrequentMenuItem extends MenuDataItem {
|
|
frequency: number;
|
|
lastUsed: number;
|
|
}
|
|
|
|
/** 本地存储键 */
|
|
const STORAGE_KEY = 'erp_frequent_menus';
|
|
/** 保留常用菜单数量 */
|
|
const MAX_FREQUENT_MENUS = 8;
|
|
/** 最低使用频率阈值 */
|
|
const MIN_FREQUENCY = 1;
|
|
|
|
/**
|
|
* 递归扁平化菜单数据,生成搜索列表
|
|
*/
|
|
const flattenMenuData = (menuData: MenuDataItem[]): MenuDataItem[] => {
|
|
const result: MenuDataItem[] = [];
|
|
|
|
menuData.forEach((item) => {
|
|
if (item.hideInMenu) return;
|
|
|
|
const currentPath = item.path;
|
|
|
|
if (item.name) {
|
|
result.push({
|
|
...item,
|
|
path: currentPath,
|
|
});
|
|
}
|
|
|
|
if (item.children && item.children.length > 0) {
|
|
result.push(...flattenMenuData(item.children));
|
|
}
|
|
});
|
|
|
|
return result;
|
|
};
|
|
|
|
/** 从本地获取常用菜单 */
|
|
const getFrequentMenus = (): Record<string, FrequentMenuItem> => {
|
|
try {
|
|
const data = localStorage.getItem(STORAGE_KEY);
|
|
return data ? JSON.parse(data) : {};
|
|
} catch {
|
|
return {};
|
|
}
|
|
};
|
|
|
|
/** 保存常用菜单到本地 */
|
|
const saveFrequentMenus = (menus: Record<string, FrequentMenuItem>) => {
|
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(menus));
|
|
};
|
|
|
|
/**
|
|
* 记录菜单使用
|
|
*/
|
|
const recordMenuUsage = (path: string, name?: string) => {
|
|
if (!path) return;
|
|
|
|
const frequentMenus = getFrequentMenus();
|
|
const now = Date.now();
|
|
|
|
if (frequentMenus[path]) {
|
|
frequentMenus[path].frequency += 1;
|
|
frequentMenus[path].lastUsed = now;
|
|
} else {
|
|
frequentMenus[path] = {
|
|
path,
|
|
name: name || '',
|
|
frequency: 1,
|
|
lastUsed: now,
|
|
};
|
|
}
|
|
|
|
saveFrequentMenus(frequentMenus);
|
|
};
|
|
|
|
const SearchMenu: React.FC<SearchMenuProps> = ({ menuData = [] }) => {
|
|
const { styles } = useSearchMenuStyle();
|
|
const [open, setOpen] = useState(false);
|
|
const [searchValue, setSearchValue] = useState('');
|
|
|
|
// 扁平化菜单数据用于搜索
|
|
const flatMenus = useMemo(() => flattenMenuData(menuData), [menuData]);
|
|
|
|
// 根据搜索关键词过滤菜单
|
|
const searchResults = useMemo(() => {
|
|
if (!searchValue.trim()) return [];
|
|
|
|
const keyword = searchValue.toLowerCase();
|
|
return flatMenus.filter(
|
|
(item) =>
|
|
item.name?.toLowerCase().includes(keyword) ||
|
|
item.path?.toLowerCase().includes(keyword),
|
|
);
|
|
}, [flatMenus, searchValue]);
|
|
|
|
// 获取常用菜单(按频率和使用时间排序)
|
|
const frequentMenus = useMemo(() => {
|
|
const frequentData = getFrequentMenus();
|
|
|
|
// 将常用菜单与扁平化菜单合并,获取完整信息
|
|
const mergedMenus = flatMenus
|
|
.map((item) => {
|
|
const freq = frequentData[item.path!];
|
|
return freq
|
|
? {
|
|
...item,
|
|
frequency: freq.frequency,
|
|
lastUsed: freq.lastUsed,
|
|
}
|
|
: { ...item, frequency: 0, lastUsed: 0 };
|
|
})
|
|
.filter((item) => item.frequency >= MIN_FREQUENCY);
|
|
|
|
// 按频率降序,使用时间降序排序
|
|
return mergedMenus
|
|
.sort((a, b) => {
|
|
if (b.frequency !== a.frequency) {
|
|
return b.frequency - a.frequency;
|
|
}
|
|
return b.lastUsed - a.lastUsed;
|
|
})
|
|
.slice(0, MAX_FREQUENT_MENUS);
|
|
}, [flatMenus]);
|
|
|
|
// 处理菜单点击
|
|
const handleMenuClick = (path: string, name?: string) => {
|
|
if (path) {
|
|
recordMenuUsage(path, name);
|
|
history.push(path);
|
|
setOpen(false);
|
|
setSearchValue('');
|
|
}
|
|
};
|
|
|
|
// 生成 AutoComplete 的 options
|
|
const options: AutoCompleteProps['options'] = useMemo(() => {
|
|
// 如果有搜索关键词,显示搜索结果
|
|
if (searchValue.trim()) {
|
|
return searchResults.map((item) => ({
|
|
value: item.path,
|
|
label: (
|
|
<div
|
|
className={styles.searchItem}
|
|
onClick={() => handleMenuClick(item.path!, item.name)}
|
|
>
|
|
<span className={styles.menuName}>{item.name}</span>
|
|
<span className={styles.menuPath}>{item.path}</span>
|
|
</div>
|
|
),
|
|
}));
|
|
}
|
|
|
|
// 如果没有搜索关键词,显示常用菜单
|
|
if (frequentMenus.length > 0) {
|
|
return [
|
|
{
|
|
value: 'frequent-header',
|
|
label: (
|
|
<div className={styles.sectionHeader}>
|
|
<StarOutlined className={styles.sectionIcon} />
|
|
<span>常用菜单</span>
|
|
</div>
|
|
),
|
|
disabled: true,
|
|
},
|
|
...frequentMenus.map((item) => ({
|
|
value: item.path,
|
|
label: (
|
|
<div
|
|
className={styles.searchItem}
|
|
onClick={() => handleMenuClick(item.path!, item.name)}
|
|
>
|
|
<div className={styles.menuRow}>
|
|
<span className={styles.menuName}>{item.name}</span>
|
|
<span className={styles.frequency}>{item.frequency} 次</span>
|
|
</div>
|
|
<span className={styles.menuPath}>{item.path}</span>
|
|
</div>
|
|
),
|
|
})),
|
|
];
|
|
}
|
|
|
|
// 没有常用菜单时显示提示
|
|
return [
|
|
{
|
|
value: 'empty-header',
|
|
label: (
|
|
<div className={styles.sectionHeader}>
|
|
<ClockCircleOutlined className={styles.sectionIcon} />
|
|
<span>暂无常用菜单</span>
|
|
</div>
|
|
),
|
|
disabled: true,
|
|
},
|
|
{
|
|
value: 'empty-tip',
|
|
label: (
|
|
<div className={styles.emptyTip}>点击菜单后会自动记录使用频率</div>
|
|
),
|
|
disabled: true,
|
|
},
|
|
];
|
|
}, [searchResults, searchValue, frequentMenus, styles]);
|
|
|
|
// 处理搜索值变化
|
|
const handleSearch: AutoCompleteProps['onSearch'] = (value) => {
|
|
setSearchValue(value);
|
|
};
|
|
|
|
// 处理下拉框显示状态变化
|
|
const handleDropdownOpen = (open: boolean) => {
|
|
setOpen(open);
|
|
if (!open) {
|
|
setSearchValue('');
|
|
}
|
|
};
|
|
|
|
// 处理选中选项
|
|
const handleSelect: AutoCompleteProps['onSelect'] = (value) => {
|
|
const selectedMenu = flatMenus.find((item) => item.path === value);
|
|
handleMenuClick(value as string, selectedMenu?.name);
|
|
};
|
|
|
|
return (
|
|
<div className={styles.searchMenuContainer}>
|
|
<AutoComplete
|
|
open={open}
|
|
onDropdownVisibleChange={handleDropdownOpen}
|
|
onSearch={handleSearch}
|
|
onSelect={handleSelect}
|
|
options={options}
|
|
dropdownClassName={styles.dropdown}
|
|
dropdownMatchSelectWidth={320}
|
|
backfill
|
|
>
|
|
<Input
|
|
placeholder="搜索菜单..."
|
|
prefix={<SearchOutlined className={styles.searchIcon} />}
|
|
allowClear
|
|
className={styles.searchInput}
|
|
onClick={() => setOpen(true)}
|
|
/>
|
|
</AutoComplete>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default SearchMenu;
|