动态更新面包屑导航

This commit is contained in:
linxd
2025-07-03 14:40:22 +08:00
parent c8b28a4a29
commit 7114466b26
10 changed files with 586 additions and 511 deletions

123
README.md Normal file
View File

@ -0,0 +1,123 @@
# 动态面包屑导航实现说明
## 功能介绍
本项目实现了动态修改页面面包屑名称的功能,特别适用于新增和修改页面共用一个组件的场景。通过 dva 状态管理,可以在具体页面中动态设置面包屑导航的最后一级显示文本。
## 实现原理
1. 通过 `breadcrumb` 模型管理面包屑状态
2. 使用 `connect``breadcrumb` 状态连接到布局组件
3. 在页面组件中根据不同条件(如是否有 id 参数)来动态设置面包屑名称
## 代码结构
- `src/models/breadcrumb.ts` - 面包屑状态模型
- `src/layouts/BasicLayout.tsx` - 支持动态面包屑的布局组件
## 使用示例
### 实际项目中的使用方式 (supplierAnnualTemplateManageAdd.tsx)
```tsx
import React, { useEffect } from 'react';
import { useIntl, connect } from 'umi';
import type { Dispatch } from 'umi';
import type { BreadcrumbState } from '@/models/breadcrumb';
// 组件 props 接口定义
interface PageProps extends ConnectProps {
breadcrumb: BreadcrumbState; // dva model状态
dispatch: Dispatch; // dva dispatch方法
}
const YourPage: React.FC<PageProps> = ({ dispatch }) => {
const intl = useIntl();
const location = useLocation();
// 设置面包屑名称
useEffect(() => {
if (location.state?.editData?.id && dispatch) {
// 编辑模式,设置编辑相关的面包屑
dispatch({
type: 'breadcrumb/updateBreadcrumbName',
payload: intl.formatMessage({ id: "yourNamespace.add.edit" }),
});
} else {
// 新增模式,设置新增相关的面包屑
dispatch({
type: 'breadcrumb/updateBreadcrumbName',
payload: intl.formatMessage({ id: "yourNamespace.add.title" }),
});
}
// 组件卸载时重置面包屑
return () => {
dispatch({
type: 'breadcrumb/resetBreadcrumb',
});
};
}, [dispatch, intl, location]);
// 页面其他内容...
return (
<div>
{/* 页面内容... */}
</div>
);
};
// 将dva model中的状态映射到组件props
export default connect(({ breadcrumb }: { breadcrumb: BreadcrumbState }) => ({
breadcrumb,
}))(YourPage);
```
### 关键实现点
1. 在页面组件中通过 connect 连接到 dva 状态:
```tsx
// 将dva model中的状态映射到组件props
export default connect(({ breadcrumb }: { breadcrumb: BreadcrumbState }) => ({
breadcrumb,
}))(YourPage);
```
2. 在 useEffect 中动态设置面包屑名称:
```tsx
useEffect(() => {
// 根据条件设置不同的面包屑名称
if (location.state?.editData?.id) {
dispatch({
type: 'breadcrumb/updateBreadcrumbName',
payload: intl.formatMessage({ id: "yourNamespace.add.edit" }),
});
} else {
dispatch({
type: 'breadcrumb/updateBreadcrumbName',
payload: intl.formatMessage({ id: "yourNamespace.add.title" }),
});
}
// 组件卸载时重置面包屑
return () => {
dispatch({
type: 'breadcrumb/resetBreadcrumb',
});
};
}, [dispatch, intl, location]);
```
## 注意事项
1. **payload 必须是字符串**:传递给 `updateBreadcrumbName` action 的 payload 必须是字符串类型,不能是对象或 React 组件
- 正确:`payload: intl.formatMessage({ id: "menu.edit" })`
- 错误:`payload: { breadcrumbName: <FormattedMessage id="menu.edit" /> }`
2. **重置面包屑**:确保在组件卸载时调用 `resetBreadcrumb` 以重置面包屑状态
3. **国际化处理**:使用 `intl.formatMessage()` 方法处理国际化文本,不要在 payload 中直接使用 FormattedMessage 组件
4. **依赖项管理**:确保 useEffect 的依赖项包含 `dispatch`, `intl` 和用于条件判断的变量(如 `location`

View File

@ -2,11 +2,13 @@
import React from 'react';
import ProLayout, { PageContainer } from '@ant-design/pro-layout';
import { Link, useLocation, useIntl, useHistory } from 'umi';
import { connect } from 'dva';
import defaultSettings from '../../config/defaultSettings';
import routes from '../../config/router.config'; // 引入你的自定义路由结构
import { ConfigProvider, Breadcrumb } from 'antd';
import HeaderComponent from './Header';
import IconFont from '@/components/IconFont/IconFont';
import { BreadcrumbState } from '@/models/breadcrumb';
const MenuRender = (item: any, isSubMenu: boolean) => {
@ -32,22 +34,29 @@ const MenuRender = (item: any, isSubMenu: boolean) => {
);
};
const BreadcrumbRender = (breadcrumb: any, intl: any, history: any) => {
const breadcrumbRoutes = breadcrumb?.routes;
const BreadcrumbRender = (routeBreadcrumb: any, intl: any, history: any, dynamicBreadcrumbName: string | null) => {
const breadcrumbRoutes = routeBreadcrumb?.routes;
return (
<Breadcrumb>
<Breadcrumb.Item
onClick={() => {
history.push('/');
}}
style={{ cursor: 'pointer' }}
>
{intl.formatMessage({ id: 'menu.首页' })}
<span style={{ cursor: 'pointer' }}>
{intl.formatMessage({ id: 'menu.首页' })}
</span>
</Breadcrumb.Item>
{breadcrumbRoutes?.map((item: any) => {
{breadcrumbRoutes?.map((item: any, index: number) => {
// 判断是否是最后一个面包屑项且存在动态名称
const isLastItem = index === (breadcrumbRoutes.length - 1);
const displayName = (isLastItem && dynamicBreadcrumbName)
? dynamicBreadcrumbName
: intl.formatMessage({ id: `menu.${item.breadcrumbName}` || '' });
return (
<Breadcrumb.Item key={item.path}>
{intl.formatMessage({ id: `menu.${item.breadcrumbName}` || '' })}
{displayName}
</Breadcrumb.Item>
);
})}
@ -55,7 +64,13 @@ const BreadcrumbRender = (breadcrumb: any, intl: any, history: any) => {
);
};
const BasicLayout: React.FC = (props) => {
interface BasicLayoutProps {
children: React.ReactNode;
breadcrumb: BreadcrumbState;
}
const BasicLayout: React.FC<BasicLayoutProps> = (props) => {
const { children, breadcrumb } = props;
const location = useLocation();
const intl = useIntl();
const history = useHistory();
@ -81,14 +96,18 @@ const BasicLayout: React.FC = (props) => {
ghost={true}
header={{
title: false,
breadcrumbRender: ({ breadcrumb }) => BreadcrumbRender(breadcrumb, intl, history),
breadcrumbRender: ({ breadcrumb: routeBreadcrumb }) =>
BreadcrumbRender(routeBreadcrumb, intl, history, breadcrumb.breadcrumbName),
}}
>
{props.children}
{children}
</PageContainer>
</ProLayout>
</ConfigProvider>
);
};
export default BasicLayout;
export default connect(({ breadcrumb }: { breadcrumb: BreadcrumbState }) => ({
breadcrumb
}))(BasicLayout);

View File

@ -1,64 +0,0 @@
import React, { useEffect } from 'react';
// import Header from './Header';
import { Layout, Breadcrumb } from 'antd';
import { useLocation, useIntl, Link, connect } from 'umi';
import type { ConnectProps, Dispatch } from 'umi';
const { Header, Sider, Content } = Layout;
//导入logo图片
import HeaderComponent from './Header';
import SiderMenu from './SiderMenu';
import './layout.less';
import type { BreadcrumbModelState } from '@/models/breadcrumb';
interface LayoutIndexProps extends ConnectProps {
breadcrumb: BreadcrumbModelState;
dispatch: Dispatch;
}
const LayoutIndex: React.FC<LayoutIndexProps> = (props) => {
const { children, breadcrumb, dispatch } = props;
const location = useLocation();
const intl = useIntl();
// 当路由变化时更新面包屑
useEffect(() => {
console.log(location)
dispatch({
type: 'breadcrumb/updateBreadcrumbs',
payload: { pathname: location.pathname, intl },
});
}, [location.pathname, intl, dispatch]);
return (
<>
<Layout>
<Header className="header">
<HeaderComponent />
</Header>
<Layout>
<Sider width={200} theme="light">
<SiderMenu />
</Sider>
<Layout className="layout-content">
<Breadcrumb style={{ margin: '10px 0' }}>
{breadcrumb.breadcrumbs.map((breadcrumbItem, index) => (
<Breadcrumb.Item key={breadcrumbItem.path}>
{index < breadcrumb.breadcrumbs.length - 1 ? (
<Link to={breadcrumbItem.path}>{breadcrumbItem.breadcrumbName}</Link>
) : (
breadcrumbItem.breadcrumbName
)}
</Breadcrumb.Item>
))}
</Breadcrumb>
<Content>{children}</Content>
</Layout>
</Layout>
</Layout>
</>
);
};
export default connect(({ breadcrumb }: { breadcrumb: BreadcrumbModelState }) => ({
breadcrumb,
}))(LayoutIndex);

View File

@ -1,127 +0,0 @@
import React, { useEffect, useState } from 'react';
import { Menu, Tooltip } from 'antd';
import { useIntl, Link, useHistory } from 'umi';
import IconFont from '@/components/IconFont/IconFont';
import routerConfig from '../../config/router.config';
// 路由接口定义
interface IRouteItem {
path: string;
component?: string;
name?: string;
meta?: {
title?: string;
hide?: boolean;
icon?: string;
};
routes?: IRouteItem[];
children?: IRouteItem[];
redirect?: string;
}
interface IMenuItem {
label: string;
key: string;
path: string;
icon: string;
children?: IMenuItem[];
}
// 获取路由配置转换为菜单项
const generateMenuItems = (): IMenuItem[] => {
// 找到主布局下的路由
const mainLayoutRoute = routerConfig.find((route: IRouteItem) => route.path === '/');
if (!mainLayoutRoute || !mainLayoutRoute.routes) return [];
// 递归处理路由,生成菜单项
const processRoutes = (routes: IRouteItem[], parentPath: string = ''): IMenuItem[] => {
return routes
.filter((route) => !route.redirect && route.name && !route.meta?.hide)
.map((route) => {
// 构建完整路径
const routePath = route.path.startsWith('/')
? route.path
: `${parentPath}/${route.path}`.replace(/\/+/g, '/');
// 创建菜单项
const menuItem: IMenuItem = {
label: route.meta?.title ? `menu.${route.meta.title}` : `menu.${route.name || ''}`,
key: route.path || '',
path: routePath,
icon: route.meta?.icon || 'icon-liebiaomoshi',
};
// 如果有子路由,递归处理
if (route.routes && route.routes.length > 0) {
const children = processRoutes(route.routes, routePath);
if (children.length > 0) {
menuItem.children = children;
}
}
// 如果有children递归处理
if (route.children && route.children.length > 0) {
const children = processRoutes(route.children, routePath);
if (children.length > 0) {
menuItem.children = children;
}
}
return menuItem;
});
};
return processRoutes(mainLayoutRoute.routes);
};
const items: IMenuItem[] = generateMenuItems();
const SiderMenu: React.FC = (props: any) => {
//当前激活菜单
const [current, setCurrent] = useState('index');
const intl = useIntl();
const history = useHistory();
useEffect(() => {
// 获取当前激活菜单
const path = history.location.pathname;
setCurrent(path);
// if (path.split('/').length > 1) {
// setCurrent(path.split('/')[path.split('/').length - 1]);
// return;
// }
}, [history.location.pathname]);
// 递归渲染菜单项
const renderMenuItems = (menuItems: IMenuItem[]) => {
return menuItems.map((item: IMenuItem) =>
item.children && item.children.length > 0 ? (
<Menu.SubMenu
key={item.path}
title={
<Tooltip title={intl.formatMessage({ id: item.label })} placement="right">
<>{intl.formatMessage({ id: item.label })}</>
</Tooltip>
}
icon={<IconFont type={item.icon} />}
>
{renderMenuItems(item.children)}
</Menu.SubMenu>
) : (
<Menu.Item key={item.path} icon={<IconFont type={item.icon} />}>
<Tooltip title={intl.formatMessage({ id: item.label })} placement="right">
<Link to={item.path}>{intl.formatMessage({ id: item.label })}</Link>
</Tooltip>
</Menu.Item>
),
);
};
return (
<div className="header-menu">
<Menu selectedKeys={[current]} mode="inline">
{renderMenuItems(items)}
</Menu>
</div>
);
};
export default SiderMenu;

View File

@ -1,136 +1,60 @@
import { Effect, Reducer } from 'umi';
export interface BreadcrumbItem {
path: string;
breadcrumbName: string;
}
type Effect = (action: { payload: any }, effects: { call: any; put: any; select: any }) => Generator<any, void, unknown>;
type Reducer<S> = (state: S, action: { payload: any }) => S;
export interface BreadcrumbModelState {
breadcrumbs: BreadcrumbItem[];
export interface BreadcrumbState {
breadcrumbName: string | null;
}
export interface BreadcrumbModelType {
namespace: 'breadcrumb';
state: BreadcrumbModelState;
state: BreadcrumbState;
effects: {
updateBreadcrumbs: Effect;
updateBreadcrumbName: Effect;
resetBreadcrumb: Effect;
};
reducers: {
saveBreadcrumbs: Reducer<BreadcrumbModelState>;
setBreadcrumbName: Reducer<BreadcrumbState>;
resetState: Reducer<BreadcrumbState>;
};
}
/**
* 生成面包屑数据
* @param pathname 当前路径
* @param intl 国际化对象
*/
const generateBreadcrumbs = (pathname: string, intl: any): BreadcrumbItem[] => {
// 如果是首页,直接返回首页
if (pathname === '/' || pathname === '/index') {
return [
{
path: '/',
breadcrumbName: intl.formatMessage({ id: 'menu.首页' }, { defaultMessage: '首页' }),
},
];
}
// 构建面包屑数据
const breadcrumbs = [
{
path: '/',
breadcrumbName: intl.formatMessage({ id: 'menu.首页' }, { defaultMessage: '首页' }),
},
];
// 按照斜杠分割路径
const pathSegments = pathname.split('/').filter(segment => segment);
let currentPath = '';
// 构建每一级的路径和对应的面包屑名称
for (let i = 0; i < pathSegments.length; i++) {
const segment = pathSegments[i];
currentPath += `/${segment}`;
// 尝试使用多语言配置
// 先尝试完整路径的多语言配置
let menuKey = `menu.${pathSegments.slice(0, i + 1).join('/')}`;
let menuName = intl.formatMessage({ id: menuKey }, { defaultMessage: '' });
// 如果完整路径没有对应的多语言,则尝试使用当前段的多语言
if (!menuName) {
menuKey = `menu.${segment}`;
menuName = intl.formatMessage({ id: menuKey }, { defaultMessage: '' });
}
// 如果仍然没有找到多语言配置,尝试使用特定页面的多语言配置
if (!menuName) {
// 处理特殊情况,如数据统计下的子页面
if (pathSegments[0] === 'dataStatistics') {
switch (segment) {
case 'supplierEvaluateStatistics':
menuName = intl.formatMessage({ id: 'menu.供应商评价情况统计' }, { defaultMessage: '供应商评价情况统计' });
break;
case 'supplierAnnualStatistics':
menuName = intl.formatMessage({ id: 'menu.供应商年审情况统计' }, { defaultMessage: '供应商年审情况统计' });
break;
case 'supplierQualificationWarningStatistics':
menuName = intl.formatMessage({ id: 'menu.供应商资质预警统计' }, { defaultMessage: '供应商资质预警统计' });
break;
case 'supplierExitStatistics':
menuName = intl.formatMessage({ id: 'menu.供应商清退情况统计' }, { defaultMessage: '供应商清退情况统计' });
break;
default:
menuName = segment;
}
} else if (pathSegments[0] === 'supplierEvaluate') {
// 处理供应商评价模块
menuName = intl.formatMessage({ id: `menu.${segment}` }, { defaultMessage: segment });
} else if (pathSegments[0] === 'supplierAnnual') {
// 处理供应商年审模块
menuName = intl.formatMessage({ id: `menu.${segment}` }, { defaultMessage: segment });
} else {
menuName = segment;
}
}
breadcrumbs.push({
path: currentPath,
breadcrumbName: menuName || segment,
});
}
return breadcrumbs;
};
const BreadcrumbModel: BreadcrumbModelType = {
namespace: 'breadcrumb',
state: {
breadcrumbs: [],
breadcrumbName: null,
},
effects: {
*updateBreadcrumbs({ payload }, { put }) {
const { pathname, intl } = payload;
console.log(pathname)
const breadcrumbs = generateBreadcrumbs(pathname, intl);
*updateBreadcrumbName({ payload }, { put }) {
yield put({
type: 'saveBreadcrumbs',
payload: breadcrumbs,
type: 'setBreadcrumbName',
payload,
});
},
*resetBreadcrumb(_, { put }) {
yield put({
type: 'resetState',
});
},
},
reducers: {
saveBreadcrumbs(state, { payload }) {
setBreadcrumbName(state: BreadcrumbState, { payload }: { payload: string }) {
return {
...state,
breadcrumbs: payload,
breadcrumbName: payload,
};
},
resetState() {
return {
breadcrumbName: null,
};
},
},
};
export default BreadcrumbModel;
export default BreadcrumbModel;

View File

@ -1,5 +1,8 @@
import globalModal from './globalModal';
import breadcrumb from './breadcrumb';
export default {
globalModal,
breadcrumb,
// ... 其他 models
};

View File

@ -8,6 +8,7 @@ import SupplierSelectStep from './components/SupplierSelectStep';
import EvaluatorSelectStep from './components/EvaluatorSelectStep';
import styles from './supplierAnnualTaskManageAdd.less';
import type { SupplierTaskModelState } from '@/models/supplierAnnualTaskManage';
import type { BreadcrumbState } from '@/models/breadcrumb';
const { Step } = Steps;
@ -38,7 +39,7 @@ interface PageProps extends ConnectProps {
* 供应商任务管理添加/编辑组件
* 使用步骤式表单引导用户完成添加或编辑任务的流程
*/
const SupplierTaskManageAdd: React.FC<PageProps> = ({ supplierAnnualTaskManage, dispatch }) => {
const SupplierTaskManageAdd: React.FC<PageProps> = ({ supplierAnnualTaskManage, dispatch, breadcrumb }) => {
const intl = useIntl();
// 获取dva model中的状态
const { currentStep, loading, detailLoading } = supplierAnnualTaskManage;
@ -85,14 +86,23 @@ const SupplierTaskManageAdd: React.FC<PageProps> = ({ supplierAnnualTaskManage,
* 组件卸载时重置状态
*/
useEffect(() => {
if (isEditMode && dispatch) {
dispatch({
type: 'breadcrumb/updateBreadcrumbName',
payload: intl.formatMessage({ id: 'supplierAnnualTaskManage.edit.title' }),
});
}
return () => {
if (dispatch) {
dispatch({
type: 'supplierAnnualTaskManage/resetState',
});
dispatch({
type: 'breadcrumb/resetBreadcrumb',
});
}
};
}, [dispatch]);
}, [dispatch, isEditMode]);
// 步骤配置,定义每个步骤的标题、描述和内容组件
const steps = [
@ -274,7 +284,8 @@ const SupplierTaskManageAdd: React.FC<PageProps> = ({ supplierAnnualTaskManage,
// 将dva model中的状态映射到组件props
export default connect(
({ supplierAnnualTaskManage }: { supplierAnnualTaskManage: SupplierTaskModelState }) => ({
({ supplierAnnualTaskManage, breadcrumb }: { supplierAnnualTaskManage: SupplierTaskModelState, breadcrumb: BreadcrumbState }) => ({
supplierAnnualTaskManage,
breadcrumb,
}),
)(SupplierTaskManageAdd);

View File

@ -17,23 +17,17 @@ import {
Popconfirm,
Modal,
} from 'antd';
import { history, useLocation, useIntl } from 'umi';
import {
ArrowLeftOutlined,
SaveOutlined,
PlusOutlined,
DeleteOutlined,
} from '@ant-design/icons';
import { history, useLocation, useIntl, connect, FormattedMessage } from 'umi';
import type { ConnectProps, Dispatch } from 'umi';
import { ArrowLeftOutlined, SaveOutlined, PlusOutlined, DeleteOutlined } from '@ant-design/icons';
import CategorySelector from '@/components/CategorySelector';
import {
AnnualTemplateStatus,
AnnualTemplateStatusText,
} from '@/dicts/supplierAnnualDict';
import type { BreadcrumbState } from '@/models/breadcrumb';
import { AnnualTemplateStatus, AnnualTemplateStatusText } from '@/dicts/supplierAnnualDict';
import {
addAnnualTemplate,
updateAnnualTemplate,
getAnnualTemplateDetail,
getAllAnnualTemplates
getAllAnnualTemplates,
} from '@/servers/api/supplierAnnual';
import styles from './supplierAnnualTemplateManage.less';
@ -53,33 +47,30 @@ const CategoryLimitationType = {
UNIVERSAL: '0', // 不限
LIMITED: '1', // 限制
};
// 品类限制类型文本
const CategoryLimitationTypeText = {
[CategoryLimitationType.UNIVERSAL]: '不限',
[CategoryLimitationType.LIMITED]: '限制',
};
// 是否星号项常量
const StarOptions = {
YES: '1',
NO: '0',
};
// 是否星号项文本
const StarOptionsText = {
[StarOptions.YES]: '是',
[StarOptions.NO]: '否',
};
const SupplierAnnualTemplateManageAdd: React.FC = () => {
interface PageProps extends ConnectProps {
breadcrumb: BreadcrumbState; // dva model状态
dispatch: Dispatch; // dva dispatch方法
}
const SupplierAnnualTemplateManageAdd: React.FC<PageProps> = ({ breadcrumb, dispatch }) => {
const intl = useIntl();
const [form] = Form.useForm();
const [loading, setLoading] = useState<boolean>(false);
const [isEdit, setIsEdit] = useState<boolean>(false);
const [templateDetail, setTemplateDetail] = useState<supplierAnnualTemplateManage.TemplateDetailData | null>(null);
const [templateList, setTemplateList] = useState<supplierAnnualTemplateManage.TemplateRecord[]>([]);
const [indicatorList, setIndicatorList] = useState<supplierAnnualTemplateManage.IndicatorItem[]>([]);
const [templateDetail, setTemplateDetail] =
useState<supplierAnnualTemplateManage.TemplateDetailData | null>(null);
const [templateList, setTemplateList] = useState<supplierAnnualTemplateManage.TemplateRecord[]>(
[],
);
const [indicatorList, setIndicatorList] = useState<supplierAnnualTemplateManage.IndicatorItem[]>(
[],
);
// 获取路由传递的数据
const location = useLocation<LocationState>();
@ -90,9 +81,22 @@ const SupplierAnnualTemplateManageAdd: React.FC = () => {
setLoading(true);
const res = await getAllAnnualTemplates();
if (res.success && res.data) {
setTemplateList(res.data);
// 如果是修改,需要过滤掉自己
if (location.state?.editData) {
setTemplateList(
res.data.filter(
(template: supplierAnnualTemplateManage.TemplateRecord) =>
template.id !== location.state.editData?.id,
),
);
} else {
setTemplateList(res.data);
}
} else {
message.error(res.message || intl.formatMessage({ id: 'supplierAnnualTemplateManage.list.getListFailed' }));
message.error(
res.message ||
intl.formatMessage({ id: 'supplierAnnualTemplateManage.list.getListFailed' }),
);
}
setLoading(false);
} catch (error) {
@ -124,7 +128,10 @@ const SupplierAnnualTemplateManageAdd: React.FC = () => {
setIndicatorList(res.data.indicatorList);
}
} else {
message.error(res.message || intl.formatMessage({ id: 'supplierAnnualTemplateManage.add.getDetailFailed' }));
message.error(
res.message ||
intl.formatMessage({ id: 'supplierAnnualTemplateManage.add.getDetailFailed' }),
);
}
setLoading(false);
} catch (error) {
@ -133,7 +140,21 @@ const SupplierAnnualTemplateManageAdd: React.FC = () => {
setLoading(false);
}
};
useEffect(() => {
if (location.state?.editData?.id && dispatch) {
dispatch({
type: 'breadcrumb/updateBreadcrumbName',
payload: intl.formatMessage({ id: "supplierAnnualTemplateManage.add.edit" }),
});
}
// 组件卸载时重置面包屑
return () => {
dispatch({
type: 'breadcrumb/resetBreadcrumb',
});
};
}, [dispatch, intl, location]);
// 初始化编辑数据
useEffect(() => {
// 获取所有模板列表
@ -176,20 +197,27 @@ const SupplierAnnualTemplateManageAdd: React.FC = () => {
}
if (res && res.success) {
message.success(isEdit
? intl.formatMessage({ id: 'supplierAnnualTemplateManage.add.submitSuccess' })
: intl.formatMessage({ id: 'supplierAnnualTemplateManage.add.saveSuccess' }));
message.success(
isEdit
? intl.formatMessage({ id: 'supplierAnnualTemplateManage.add.submitSuccess' })
: intl.formatMessage({ id: 'supplierAnnualTemplateManage.add.saveSuccess' }),
);
history.goBack();
} else {
message.error(res?.message || (isEdit
? intl.formatMessage({ id: 'supplierAnnualTemplateManage.add.submitFailed' })
: intl.formatMessage({ id: 'supplierAnnualTemplateManage.add.saveFailed' })));
message.error(
res?.message ||
(isEdit
? intl.formatMessage({ id: 'supplierAnnualTemplateManage.add.submitFailed' })
: intl.formatMessage({ id: 'supplierAnnualTemplateManage.add.saveFailed' })),
);
}
} catch (error) {
console.error('提交失败:', error);
message.error(isEdit
? intl.formatMessage({ id: 'supplierAnnualTemplateManage.add.submitFailed' })
: intl.formatMessage({ id: 'supplierAnnualTemplateManage.add.saveFailed' }));
message.error(
isEdit
? intl.formatMessage({ id: 'supplierAnnualTemplateManage.add.submitFailed' })
: intl.formatMessage({ id: 'supplierAnnualTemplateManage.add.saveFailed' }),
);
} finally {
setLoading(false);
}
@ -199,17 +227,21 @@ const SupplierAnnualTemplateManageAdd: React.FC = () => {
const handleSubmit = async (values: any) => {
// 检查指标列表
if (!indicatorList || indicatorList.length === 0) {
message.error(intl.formatMessage({ id: 'supplierAnnualTemplateManage.add.atLeastOneIndicator' }));
message.error(
intl.formatMessage({ id: 'supplierAnnualTemplateManage.add.atLeastOneIndicator' }),
);
return;
}
// 检查指标名称不能为空
const emptyNameIndex = indicatorList.findIndex(item => !item.itemName);
const emptyNameIndex = indicatorList.findIndex((item) => !item.itemName);
if (emptyNameIndex !== -1) {
message.error(intl.formatMessage(
{ id: 'supplierAnnualTemplateManage.add.indicatorNameRequired' },
{ index: emptyNameIndex + 1 }
));
message.error(
intl.formatMessage(
{ id: 'supplierAnnualTemplateManage.add.indicatorNameRequired' },
{ index: emptyNameIndex + 1 },
),
);
return;
}
@ -235,7 +267,7 @@ const SupplierAnnualTemplateManageAdd: React.FC = () => {
cancelText: intl.formatMessage({ id: 'supplierAnnualTemplateManage.common.cancel' }),
onOk: async () => {
await submitFormData(submitData);
}
},
});
};
@ -255,20 +287,27 @@ const SupplierAnnualTemplateManageAdd: React.FC = () => {
// 复制指标列表
if (res.data.indicatorList && res.data.indicatorList.length > 0) {
const copiedList = JSON.parse(JSON.stringify(res.data.indicatorList)).map((item: any) => {
// 删除id防止ID冲突
delete item.id;
return item;
});
const copiedList = JSON.parse(JSON.stringify(res.data.indicatorList)).map(
(item: any) => {
// 删除id防止ID冲突
delete item.id;
return item;
},
);
setIndicatorList(copiedList);
}
} else {
message.error(res.message || intl.formatMessage({ id: 'supplierAnnualTemplateManage.add.getDetailFailed' }));
message.error(
res.message ||
intl.formatMessage({ id: 'supplierAnnualTemplateManage.add.getDetailFailed' }),
);
}
setLoading(false);
} catch (error) {
console.error('获取模板详情失败:', error);
message.error(intl.formatMessage({ id: 'supplierAnnualTemplateManage.add.getDetailFailed' }));
message.error(
intl.formatMessage({ id: 'supplierAnnualTemplateManage.add.getDetailFailed' }),
);
setLoading(false);
}
}
@ -320,7 +359,9 @@ const SupplierAnnualTemplateManageAdd: React.FC = () => {
<Input
value={text}
onChange={(e) => handleIndicatorChange(index, 'itemName', e.target.value)}
placeholder={`${intl.formatMessage({ id: 'supplierAnnualTemplateManage.common.pleaseInput' })}${intl.formatMessage({ id: 'supplierAnnualTemplateManage.add.checkItem' })}`}
placeholder={`${intl.formatMessage({
id: 'supplierAnnualTemplateManage.common.pleaseInput',
})}${intl.formatMessage({ id: 'supplierAnnualTemplateManage.add.checkItem' })}`}
/>
),
},
@ -335,8 +376,12 @@ const SupplierAnnualTemplateManageAdd: React.FC = () => {
onChange={(value) => handleIndicatorChange(index, 'isStar', value)}
className={styles.starSelector}
>
<Option value={StarOptions.YES}>{intl.formatMessage({ id: 'supplierAnnualTemplateManage.common.yes' })}</Option>
<Option value={StarOptions.NO}>{intl.formatMessage({ id: 'supplierAnnualTemplateManage.common.no' })}</Option>
<Option value={StarOptions.YES}>
{intl.formatMessage({ id: 'supplierAnnualTemplateManage.common.yes' })}
</Option>
<Option value={StarOptions.NO}>
{intl.formatMessage({ id: 'supplierAnnualTemplateManage.common.no' })}
</Option>
</Select>
),
},
@ -355,12 +400,19 @@ const SupplierAnnualTemplateManageAdd: React.FC = () => {
/>
)}
<Popconfirm
title={intl.formatMessage({ id: 'supplierAnnualTemplateManage.modal.deleteConfirmContent' })}
title={intl.formatMessage({
id: 'supplierAnnualTemplateManage.modal.deleteConfirmContent',
})}
onConfirm={() => handleDeleteIndicator(index)}
okText={intl.formatMessage({ id: 'supplierAnnualTemplateManage.common.confirm' })}
cancelText={intl.formatMessage({ id: 'supplierAnnualTemplateManage.common.cancel' })}
>
<Button type="link" danger icon={<DeleteOutlined />} title={intl.formatMessage({ id: 'supplierAnnualTemplateManage.add.deleteIndicator' })} />
<Button
type="link"
danger
icon={<DeleteOutlined />}
title={intl.formatMessage({ id: 'supplierAnnualTemplateManage.add.deleteIndicator' })}
/>
</Popconfirm>
</Space>
),
@ -369,143 +421,223 @@ const SupplierAnnualTemplateManageAdd: React.FC = () => {
return (
<div className="common-container">
<div className={styles.pageHeader}>
<Title level={4} style={{ margin: 0 }}>
{isEdit
? intl.formatMessage({ id: 'supplierAnnualTemplateManage.add.edit' })
: intl.formatMessage({ id: 'supplierAnnualTemplateManage.add.title' })
}
</Title>
<Button type="link" icon={<ArrowLeftOutlined />} onClick={handleBack}>
{intl.formatMessage({ id: 'supplierAnnualTemplateManage.add.back' })}
</Button>
<div className={styles.pageHeader}>
<Title level={4} style={{ margin: 0 }}>
{isEdit
? intl.formatMessage({ id: 'supplierAnnualTemplateManage.add.edit' })
: intl.formatMessage({ id: 'supplierAnnualTemplateManage.add.title' })}
</Title>
<Button type="link" icon={<ArrowLeftOutlined />} onClick={handleBack}>
{intl.formatMessage({ id: 'supplierAnnualTemplateManage.add.back' })}
</Button>
</div>
<Form
form={form}
onFinish={handleSubmit}
initialValues={{
categoryLimitation: CategoryLimitationType.UNIVERSAL,
status: AnnualTemplateStatus.DRAFT,
}}
labelCol={{ span: 7 }}
wrapperCol={{ span: 17 }}
>
<Spin spinning={loading}>
<Card
title={intl.formatMessage({ id: 'supplierAnnualTemplateManage.add.basicInfo' })}
bordered={false}
className={styles.innerCard}
>
<Row gutter={24}>
<Col span={8}>
<Form.Item
label={intl.formatMessage({
id: 'supplierAnnualTemplateManage.list.templateName',
})}
name="templateName"
rules={[
{
required: true,
message: `${intl.formatMessage({
id: 'supplierAnnualTemplateManage.common.pleaseInput',
})}${intl.formatMessage({
id: 'supplierAnnualTemplateManage.list.templateName',
})}`,
},
]}
>
<Input
placeholder={`${intl.formatMessage({
id: 'supplierAnnualTemplateManage.common.pleaseInput',
})}${intl.formatMessage({
id: 'supplierAnnualTemplateManage.list.templateName',
})}`}
maxLength={50}
/>
</Form.Item>
</Col>
<Col span={8}>
<Form.Item
label={intl.formatMessage({
id: 'supplierAnnualTemplateManage.add.categoryLimitation',
})}
name="categoryLimitation"
rules={[
{
required: true,
message: `${intl.formatMessage({
id: 'supplierAnnualTemplateManage.common.pleaseSelect',
})}${intl.formatMessage({
id: 'supplierAnnualTemplateManage.add.categoryLimitation',
})}`,
},
]}
>
<Radio.Group>
<Radio value={CategoryLimitationType.UNIVERSAL}>
{intl.formatMessage({
id: 'supplierAnnualTemplateManage.add.categoryLimitationUniversal',
})}
</Radio>
<Radio value={CategoryLimitationType.LIMITED}>
{intl.formatMessage({
id: 'supplierAnnualTemplateManage.add.categoryLimitationLimited',
})}
</Radio>
</Radio.Group>
</Form.Item>
</Col>
<Col span={8}>
<Form.Item
noStyle
shouldUpdate={(prevValues, currentValues) =>
prevValues.categoryLimitation !== currentValues.categoryLimitation
}
>
{({ getFieldValue }) => {
const categoryLimitation = getFieldValue('categoryLimitation');
return categoryLimitation === CategoryLimitationType.LIMITED ? (
<Form.Item
label={intl.formatMessage({
id: 'supplierAnnualTemplateManage.add.selectCategory',
})}
name="categoryId"
rules={[
{
required: true,
message: `${intl.formatMessage({
id: 'supplierAnnualTemplateManage.common.pleaseSelect',
})}${intl.formatMessage({
id: 'supplierAnnualTemplateManage.list.category',
})}`,
},
]}
>
<CategorySelector multiple={false} />
</Form.Item>
) : null;
}}
</Form.Item>
</Col>
</Row>
<Row gutter={24}>
<Col span={8}>
<Form.Item
label={intl.formatMessage({
id: 'supplierAnnualTemplateManage.add.selectTemplate',
})}
name="copyTemplateId"
>
<Select
placeholder={intl.formatMessage({
id: 'supplierAnnualTemplateManage.common.pleaseSelect',
})}
loading={templateList.length === 0}
onSelect={handleTemplateSelect}
>
{templateList.map((template) =>
template.id ? (
<Option key={template.id} value={template.id}>
{template.templateName}
</Option>
) : null,
)}
</Select>
</Form.Item>
</Col>
<Col span={8}>
<Form.Item
label={intl.formatMessage({ id: 'supplierAnnualTemplateManage.list.status' })}
name="status"
rules={[
{
required: true,
message: `${intl.formatMessage({
id: 'supplierAnnualTemplateManage.common.pleaseSelect',
})}${intl.formatMessage({ id: 'supplierAnnualTemplateManage.list.status' })}`,
},
]}
>
<Radio.Group>
<Radio value={AnnualTemplateStatus.DRAFT}>
{AnnualTemplateStatusText[AnnualTemplateStatus.DRAFT]}
</Radio>
<Radio value={AnnualTemplateStatus.ENABLED}>
{AnnualTemplateStatusText[AnnualTemplateStatus.ENABLED]}
</Radio>
<Radio value={AnnualTemplateStatus.DISABLED}>
{AnnualTemplateStatusText[AnnualTemplateStatus.DISABLED]}
</Radio>
</Radio.Group>
</Form.Item>
</Col>
</Row>
</Card>
<Divider />
<Card
title={intl.formatMessage({ id: 'supplierAnnualTemplateManage.add.indicatorInfo' })}
bordered={false}
className={styles.innerCard}
>
<Table
columns={columns}
dataSource={indicatorList}
bordered
rowKey="orderBy"
size="middle"
pagination={false}
locale={{
emptyText: intl.formatMessage({
id: 'supplierAnnualTemplateManage.add.noIndicatorData',
}),
}}
className={styles.indicatorTable}
/>
</Card>
</Spin>
<div className={styles.formActions}>
<Space>
<Button onClick={handleBack}>
{intl.formatMessage({ id: 'supplierAnnualTemplateManage.common.cancel' })}
</Button>
<Button type="primary" htmlType="submit" loading={loading} icon={<SaveOutlined />}>
{isEdit
? intl.formatMessage({ id: 'supplierAnnualTemplateManage.add.update' })
: intl.formatMessage({ id: 'supplierAnnualTemplateManage.add.save' })}
</Button>
</Space>
</div>
<Form
form={form}
onFinish={handleSubmit}
initialValues={{
categoryLimitation: CategoryLimitationType.UNIVERSAL,
status: AnnualTemplateStatus.DRAFT,
}}
labelCol={{ span: 7 }}
wrapperCol={{ span: 17 }}
>
<Spin spinning={loading}>
<Card title={intl.formatMessage({ id: 'supplierAnnualTemplateManage.add.basicInfo' })} bordered={false} className={styles.innerCard}>
<Row gutter={24}>
<Col span={8}>
<Form.Item
label={intl.formatMessage({ id: 'supplierAnnualTemplateManage.list.templateName' })}
name="templateName"
rules={[{ required: true, message: `${intl.formatMessage({ id: 'supplierAnnualTemplateManage.common.pleaseInput' })}${intl.formatMessage({ id: 'supplierAnnualTemplateManage.list.templateName' })}` }]}
>
<Input placeholder={`${intl.formatMessage({ id: 'supplierAnnualTemplateManage.common.pleaseInput' })}${intl.formatMessage({ id: 'supplierAnnualTemplateManage.list.templateName' })}`} maxLength={50} />
</Form.Item>
</Col>
<Col span={8}>
<Form.Item
label={intl.formatMessage({ id: 'supplierAnnualTemplateManage.add.categoryLimitation' })}
name="categoryLimitation"
rules={[{ required: true, message: `${intl.formatMessage({ id: 'supplierAnnualTemplateManage.common.pleaseSelect' })}${intl.formatMessage({ id: 'supplierAnnualTemplateManage.add.categoryLimitation' })}` }]}
>
<Radio.Group>
<Radio value={CategoryLimitationType.UNIVERSAL}>
{intl.formatMessage({ id: 'supplierAnnualTemplateManage.add.categoryLimitationUniversal' })}
</Radio>
<Radio value={CategoryLimitationType.LIMITED}>
{intl.formatMessage({ id: 'supplierAnnualTemplateManage.add.categoryLimitationLimited' })}
</Radio>
</Radio.Group>
</Form.Item>
</Col>
<Col span={8}>
<Form.Item
noStyle
shouldUpdate={(prevValues, currentValues) =>
prevValues.categoryLimitation !== currentValues.categoryLimitation
}
>
{({ getFieldValue }) => {
const categoryLimitation = getFieldValue('categoryLimitation');
return categoryLimitation === CategoryLimitationType.LIMITED ? (
<Form.Item
label={intl.formatMessage({ id: 'supplierAnnualTemplateManage.add.selectCategory' })}
name="categoryId"
rules={[{ required: true, message: `${intl.formatMessage({ id: 'supplierAnnualTemplateManage.common.pleaseSelect' })}${intl.formatMessage({ id: 'supplierAnnualTemplateManage.list.category' })}` }]}
>
<CategorySelector multiple={false} />
</Form.Item>
) : null;
}}
</Form.Item>
</Col>
</Row>
<Row gutter={24}>
<Col span={8}>
<Form.Item
label={intl.formatMessage({ id: 'supplierAnnualTemplateManage.add.selectTemplate' })}
name="copyTemplateId"
>
<Select
placeholder={intl.formatMessage({ id: 'supplierAnnualTemplateManage.common.pleaseSelect' })}
loading={templateList.length === 0}
onSelect={handleTemplateSelect}
>
{templateList.map(template => (
template.id ? (
<Option key={template.id} value={template.id}>
{template.templateName}
</Option>
) : null
))}
</Select>
</Form.Item>
</Col>
<Col span={8}>
<Form.Item
label={intl.formatMessage({ id: 'supplierAnnualTemplateManage.list.status' })}
name="status"
rules={[{ required: true, message: `${intl.formatMessage({ id: 'supplierAnnualTemplateManage.common.pleaseSelect' })}${intl.formatMessage({ id: 'supplierAnnualTemplateManage.list.status' })}` }]}
>
<Radio.Group>
<Radio value={AnnualTemplateStatus.DRAFT}>{AnnualTemplateStatusText[AnnualTemplateStatus.DRAFT]}</Radio>
<Radio value={AnnualTemplateStatus.ENABLED}>{AnnualTemplateStatusText[AnnualTemplateStatus.ENABLED]}</Radio>
<Radio value={AnnualTemplateStatus.DISABLED}>{AnnualTemplateStatusText[AnnualTemplateStatus.DISABLED]}</Radio>
</Radio.Group>
</Form.Item>
</Col>
</Row>
</Card>
<Divider />
<Card title={intl.formatMessage({ id: 'supplierAnnualTemplateManage.add.indicatorInfo' })} bordered={false} className={styles.innerCard}>
<Table
columns={columns}
dataSource={indicatorList}
bordered
rowKey="orderBy"
size="middle"
pagination={false}
locale={{ emptyText: intl.formatMessage({ id: 'supplierAnnualTemplateManage.add.noIndicatorData' }) }}
className={styles.indicatorTable}
/>
</Card>
</Spin>
<div className={styles.formActions}>
<Space>
<Button onClick={handleBack}>{intl.formatMessage({ id: 'supplierAnnualTemplateManage.common.cancel' })}</Button>
<Button type="primary" htmlType="submit" loading={loading} icon={<SaveOutlined />}>
{isEdit ? intl.formatMessage({ id: 'supplierAnnualTemplateManage.add.update' }) : intl.formatMessage({ id: 'supplierAnnualTemplateManage.add.save' })}
</Button>
</Space>
</div>
</Form>
</Form>
</div>
);
};
export default SupplierAnnualTemplateManageAdd;
// export default SupplierAnnualTemplateManageAdd;
// 将dva model中的状态映射到组件props
export default connect(({ breadcrumb }: { breadcrumb: BreadcrumbState }) => ({
breadcrumb,
}))(SupplierAnnualTemplateManageAdd);

View File

@ -10,6 +10,7 @@ import DivisionStep from './components/DivisionStep';
import styles from './supplierTaskManageAdd.less';
import { TaskNotifyLowerUnits } from '@/dicts/supplierTaskDict';
import type { SupplierTaskModelState } from '@/models/supplierTaskManage';
import type { BreadcrumbState } from '@/models/breadcrumb';
const { Step } = Steps;
@ -40,7 +41,11 @@ interface PageProps extends ConnectProps {
* 供应商任务管理添加/编辑组件
* 使用步骤式表单引导用户完成添加或编辑任务的流程
*/
const SupplierTaskManageAdd: React.FC<PageProps> = ({ supplierTaskManage, dispatch }) => {
const SupplierTaskManageAdd: React.FC<PageProps> = ({
supplierTaskManage,
dispatch,
breadcrumb,
}) => {
const intl = useIntl();
// 获取dva model中的状态
const { currentStep, loading, detailLoading } = supplierTaskManage;
@ -70,6 +75,11 @@ const SupplierTaskManageAdd: React.FC<PageProps> = ({ supplierTaskManage, dispat
type: 'supplierTaskManage/saveMode',
payload: 'edit',
});
// 更新面包屑
dispatch({
type: 'breadcrumb/updateBreadcrumbName',
payload: intl.formatMessage({ id: 'supplierTaskManage.title.edit' }),
});
// 编辑模式,获取任务详情
dispatch({
type: 'supplierTaskManage/fetchTaskDetail',
@ -106,6 +116,9 @@ const SupplierTaskManageAdd: React.FC<PageProps> = ({ supplierTaskManage, dispat
dispatch({
type: 'supplierTaskManage/resetState',
});
dispatch({
type: 'breadcrumb/resetBreadcrumb',
});
}
};
}, [dispatch]);
@ -124,7 +137,9 @@ const SupplierTaskManageAdd: React.FC<PageProps> = ({ supplierTaskManage, dispat
},
{
title: intl.formatMessage({ id: 'supplierTaskManage.step.selectEvaluator.title' }),
description: intl.formatMessage({ id: 'supplierTaskManage.step.selectEvaluator.description' }),
description: intl.formatMessage({
id: 'supplierTaskManage.step.selectEvaluator.description',
}),
content: <EvaluatorSelectStep ref={evaluatorFormRef} />,
},
{
@ -177,7 +192,9 @@ const SupplierTaskManageAdd: React.FC<PageProps> = ({ supplierTaskManage, dispat
if (typeof errorInfo === 'string') {
message.error(errorInfo);
} else if (errorInfo && errorInfo.errorFields) {
message.error(intl.formatMessage({ id: 'supplierTaskManage.message.formValidationFailed' }));
message.error(
intl.formatMessage({ id: 'supplierTaskManage.message.formValidationFailed' }),
);
}
}
};
@ -357,7 +374,14 @@ const SupplierTaskManageAdd: React.FC<PageProps> = ({ supplierTaskManage, dispat
// 将dva model中的状态映射到组件props
export default connect(
({ supplierTaskManage }: { supplierTaskManage: SupplierTaskModelState }) => ({
({
supplierTaskManage,
breadcrumb,
}: {
supplierTaskManage: SupplierTaskModelState;
breadcrumb: BreadcrumbState;
}) => ({
supplierTaskManage,
breadcrumb,
}),
)(SupplierTaskManageAdd);

View File

@ -16,7 +16,9 @@ import {
Spin,
Modal,
} from 'antd';
import { history, useLocation, useIntl } from 'umi';
import { history, useLocation, useIntl, connect } from 'umi';
import type { Dispatch,ConnectProps } from 'umi';
import type { BreadcrumbState } from '@/models/breadcrumb';
import { ArrowLeftOutlined, SaveOutlined, ExclamationCircleOutlined } from '@ant-design/icons';
import EvaluateTemplateTable from '@/components/EvaluateTemplateTable';
import CategorySelector from '@/components/CategorySelector';
@ -50,6 +52,11 @@ interface FormValues {
[key: string]: any;
}
interface PageProps extends ConnectProps {
breadcrumb: BreadcrumbState; // dva model状态
dispatch: Dispatch; // dva dispatch方法
}
interface LocationState {
isEdit?: boolean;
editData?: SupplierTemplateManage.TemplateItem;
@ -57,7 +64,7 @@ interface LocationState {
const { Title } = Typography;
const SupplierTemplateManageAdd: React.FC = () => {
const SupplierTemplateManageAdd: React.FC<PageProps> = ({ breadcrumb, dispatch }) => {
const intl = useIntl();
const [form] = Form.useForm<FormValues>();
const [loading, setLoading] = useState<boolean>(false);
@ -79,7 +86,12 @@ const SupplierTemplateManageAdd: React.FC = () => {
try {
const res = await getAllTemplates();
if (res.success && res.data) {
setTemplateList(res.data);
// 如果是修改,需要过滤掉自己
if (location.state?.editData) {
setTemplateList(res.data.filter((template: SupplierTemplateManage.TemplateItem) => template.id !== location.state.editData?.id));
} else {
setTemplateList(res.data);
}
} else {
message.error(intl.formatMessage({ id: 'supplierTemplateManage.message.getTemplateListFailed' }) || res.message);
}
@ -122,6 +134,19 @@ const SupplierTemplateManageAdd: React.FC = () => {
setLoading(false);
}
};
useEffect(() => {
if (location.state?.editData?.id && dispatch) {
dispatch({
type: 'breadcrumb/updateBreadcrumbName',
payload: intl.formatMessage({ id: "supplierTemplateManage.edit.title" }),
});
}
return () => {
dispatch({
type: 'breadcrumb/resetBreadcrumb',
});
};
}, [dispatch, intl, location]);
// 初始化编辑数据
useEffect(() => {
// 获取所有模板列表
@ -450,4 +475,9 @@ const SupplierTemplateManageAdd: React.FC = () => {
);
};
export default SupplierTemplateManageAdd;
// export default SupplierTemplateManageAdd;
// 将dva model中的状态映射到组件props
export default connect(({ breadcrumb }: { breadcrumb: BreadcrumbState }) => ({
breadcrumb,
}))(SupplierTemplateManageAdd);