refactor(material): 重构素材库组件实现

- 移除旧的 MaterialList 组件相关配置和引用
- 删除 formatParam 工具函数的不必要导入
- 新增 MaterialLibraryModal 和 CategoryTree 组件
- 将 UploadMaterial 组件重构为使用新的素材库选择方式
- 实现素材分类树的菜单展示和选择功能
- 添加素材库弹窗的图片和列表两种视图模式
- 集成素材选择、预览和确认功能
- 移除各组件中对旧 MaterialList 的依赖配置
This commit is contained in:
shenyifei 2026-01-07 11:25:50 +08:00
parent 1429319b01
commit 7260b089e9
21 changed files with 775 additions and 495 deletions

View File

@ -2,20 +2,14 @@ import {
BizContainer, BizContainer,
BizValueType, BizValueType,
BoxSpecList, BoxSpecList,
MaterialList,
ModeType, ModeType,
ProFormBizSelect, ProFormBizSelect,
ProFormBizSelectHandles, ProFormBizSelectHandles,
ProFormUploadMaterial, ProFormUploadMaterial,
} from '@/components'; } from '@/components';
import { business } from '@/services'; import { business } from '@/services';
import { formatParam } from '@/utils/formatParam';
import { useIntl } from '@@/exports'; import { useIntl } from '@@/exports';
import { import { ProColumns, ProFormText } from '@ant-design/pro-components';
ActionType,
ProColumns,
ProFormText,
} from '@ant-design/pro-components';
import { ProDescriptionsItemProps } from '@ant-design/pro-descriptions'; import { ProDescriptionsItemProps } from '@ant-design/pro-descriptions';
import { ProListMetas } from '@ant-design/pro-list'; import { ProListMetas } from '@ant-design/pro-list';
import { Image } from 'antd'; import { Image } from 'antd';
@ -46,7 +40,6 @@ export default function BoxBrandList(props: IBoxBrandListProps) {
const intl = useIntl(); const intl = useIntl();
const intlPrefix = 'boxBrand'; const intlPrefix = 'boxBrand';
const specRef = useRef<ProFormBizSelectHandles>(null); const specRef = useRef<ProFormBizSelectHandles>(null);
const actionRef = useRef<ActionType>();
const columns: ProColumns<BusinessAPI.BoxBrandVO, BizValueType>[] = [ const columns: ProColumns<BusinessAPI.BoxBrandVO, BizValueType>[] = [
{ {
@ -132,32 +125,6 @@ export default function BoxBrandList(props: IBoxBrandListProps) {
}} }}
fieldProps={{ fieldProps={{
maxCount: 1, maxCount: 1,
actionRef: actionRef,
toolBarRender: () => [
<MaterialList
key={'create'}
ghost={true}
mode={'create'}
search={false}
onValueChange={() => actionRef.current?.reload()}
/>,
],
request: async (params, sorter, filter) => {
const { data, success, totalCount } =
await business.material.pageMaterial({
materialPageQry: formatParam<typeof params>(
params,
sorter,
filter,
),
});
return {
data: data || [],
total: totalCount,
success,
};
},
}} }}
/>, />,
@ -254,14 +221,6 @@ export default function BoxBrandList(props: IBoxBrandListProps) {
type: brandType, type: brandType,
}), }),
}, },
columnsState: {
defaultValue: {
option: { show: true },
status: { show: true },
remark: { show: false },
createdAt: { show: false },
},
},
}, },
columns, columns,
}} }}
@ -270,29 +229,11 @@ export default function BoxBrandList(props: IBoxBrandListProps) {
bordered: true, bordered: true,
//@ts-ignore //@ts-ignore
search, search,
pagination: false,
itemLayout: 'vertical',
showActions: 'hover',
params: { params: {
...(brandType !== 'ALL' && { ...(brandType !== 'ALL' && {
type: brandType, type: brandType,
}), }),
}, },
onItem: (record: any) => {
return {
onClick: async (e) => {
const { data } = await business.boxBrand.showBoxBrand({
boxBrandShowQry: {
brandId: record.brandId,
},
});
if (data) {
onSelect?.(data);
}
e.stopPropagation();
},
};
},
}, },
metas, metas,
columns, columns,

View File

@ -1,25 +1,20 @@
import { import {
BizContainer, BizContainer,
BizValueType, BizValueType,
MaterialList,
ProFormUploadMaterial, ProFormUploadMaterial,
} from '@/components'; } from '@/components';
import { business } from '@/services'; import { business } from '@/services';
import { formatParam } from '@/utils/formatParam';
import { useIntl } from '@@/exports'; import { useIntl } from '@@/exports';
import { import {
ActionType,
ProColumns, ProColumns,
ProFormText, ProFormText,
ProFormTextArea, ProFormTextArea,
} from '@ant-design/pro-components'; } from '@ant-design/pro-components';
import { ProDescriptionsItemProps } from '@ant-design/pro-descriptions'; import { ProDescriptionsItemProps } from '@ant-design/pro-descriptions';
import { useRef } from 'react';
export default function ChannelList() { export default function ChannelList() {
const intl = useIntl(); const intl = useIntl();
const intlPrefix = 'channel'; const intlPrefix = 'channel';
const actionRef = useRef<ActionType>();
const formContext = [ const formContext = [
<ProFormText <ProFormText
@ -87,32 +82,6 @@ export default function ChannelList() {
}} }}
fieldProps={{ fieldProps={{
maxCount: 1, maxCount: 1,
actionRef: actionRef,
toolBarRender: () => [
<MaterialList
key={'create'}
ghost={true}
mode={'create'}
search={false}
onValueChange={() => actionRef.current?.reload()}
/>,
],
request: async (params, sorter, filter) => {
const { data, success, totalCount } =
await business.material.pageMaterial({
materialPageQry: formatParam<typeof params>(
params,
sorter,
filter,
),
});
return {
data: data || [],
total: totalCount,
success,
};
},
}} }}
/>, />,
<ProFormUploadMaterial <ProFormUploadMaterial
@ -129,32 +98,6 @@ export default function ChannelList() {
}} }}
fieldProps={{ fieldProps={{
maxCount: 1, maxCount: 1,
actionRef: actionRef,
toolBarRender: () => [
<MaterialList
key={'create'}
ghost={true}
mode={'create'}
search={false}
onValueChange={() => actionRef.current?.reload()}
/>,
],
request: async (params, sorter, filter) => {
const { data, success, totalCount } =
await business.material.pageMaterial({
materialPageQry: formatParam<typeof params>(
params,
sorter,
filter,
),
});
return {
data: data || [],
total: totalCount,
success,
};
},
}} }}
/>, />,
<ProFormText <ProFormText

View File

@ -6,7 +6,6 @@ import {
ProFormUploadMaterial, ProFormUploadMaterial,
} from '@/components'; } from '@/components';
import { business } from '@/services'; import { business } from '@/services';
import { formatParam } from '@/utils/formatParam';
import { useIntl } from '@@/exports'; import { useIntl } from '@@/exports';
import { ProColumns, ProFormText } from '@ant-design/pro-components'; import { ProColumns, ProFormText } from '@ant-design/pro-components';
import { ProDescriptionsItemProps } from '@ant-design/pro-descriptions'; import { ProDescriptionsItemProps } from '@ant-design/pro-descriptions';
@ -98,22 +97,6 @@ export default function CompanyList(props: ICompanyListProps) {
}} }}
fieldProps={{ fieldProps={{
maxCount: 1, maxCount: 1,
request: async (params, sorter, filter) => {
const { data, success, totalCount } =
await business.material.pageMaterial({
materialPageQry: formatParam<typeof params>(
params,
sorter,
filter,
),
});
return {
data: data || [],
total: totalCount,
success,
};
},
}} }}
/>, />,
<ProFormText <ProFormText

View File

@ -3,23 +3,20 @@ import {
BizValueType, BizValueType,
EmployeeDisable, EmployeeDisable,
EmployeeRoleUpdate, EmployeeRoleUpdate,
MaterialList,
ModeType, ModeType,
ProFormUploadMaterial, ProFormUploadMaterial,
RestPassword, RestPassword,
} from '@/components'; } from '@/components';
import { business } from '@/services'; import { business } from '@/services';
import { aesEncrypt } from '@/utils/aes'; import { aesEncrypt } from '@/utils/aes';
import { formatParam } from '@/utils/formatParam';
import { useIntl } from '@@/exports'; import { useIntl } from '@@/exports';
import { import {
ActionType,
ProColumns, ProColumns,
ProFormSelect, ProFormSelect,
ProFormText, ProFormText,
} from '@ant-design/pro-components'; } from '@ant-design/pro-components';
import { ProDescriptionsItemProps } from '@ant-design/pro-descriptions'; import { ProDescriptionsItemProps } from '@ant-design/pro-descriptions';
import React, { useRef } from 'react'; import React from 'react';
export interface IEmployeeListProps { export interface IEmployeeListProps {
ghost?: boolean; ghost?: boolean;
@ -33,7 +30,6 @@ const EmployeeList: React.FC = (props: IEmployeeListProps) => {
const { ghost = false, search = true, mode = 'page', onValueChange } = props; const { ghost = false, search = true, mode = 'page', onValueChange } = props;
const intl = useIntl(); const intl = useIntl();
const intlPrefix = 'employee'; const intlPrefix = 'employee';
const actionRef = useRef<ActionType>();
const columns: ProColumns<BusinessAPI.EmployeeVO, BizValueType>[] = [ const columns: ProColumns<BusinessAPI.EmployeeVO, BizValueType>[] = [
{ {
@ -187,22 +183,6 @@ const EmployeeList: React.FC = (props: IEmployeeListProps) => {
}} }}
fieldProps={{ fieldProps={{
maxCount: 1, maxCount: 1,
request: async (params, sorter, filter) => {
const { data, success, totalCount } =
await business.material.pageMaterial({
materialPageQry: formatParam<typeof params>(
params,
sorter,
filter,
),
});
return {
data: data || [],
total: totalCount,
success,
};
},
}} }}
/>, />,
<ProFormText <ProFormText
@ -408,32 +388,6 @@ const EmployeeList: React.FC = (props: IEmployeeListProps) => {
}} }}
fieldProps={{ fieldProps={{
maxCount: 1, maxCount: 1,
actionRef: actionRef,
toolBarRender: () => [
<MaterialList
key={'create'}
ghost={true}
mode={'create'}
search={false}
onValueChange={() => actionRef.current?.reload()}
/>,
],
request: async (params, sorter, filter) => {
const { data, success, totalCount } =
await business.material.pageMaterial({
materialPageQry: formatParam<typeof params>(
params,
sorter,
filter,
),
});
return {
data: data || [],
total: totalCount,
success,
};
},
}} }}
/>, />,
]; ];

View File

@ -1,21 +1,16 @@
import { UploadMaterial } from '@/components';
import { import {
ActionType,
ProFormDependency, ProFormDependency,
ProFormField, ProFormField,
ProFormItemProps, ProFormItemProps,
ProTableProps,
} from '@ant-design/pro-components'; } from '@ant-design/pro-components';
import { UploadMaterial } from '@/components'; import React from 'react';
import React, { MutableRefObject } from 'react';
interface ProFormUploadMaterialProps extends ProFormItemProps { interface ProFormUploadMaterialProps extends ProFormItemProps {
fieldProps: { fieldProps: {
maxCount: number; maxCount: number;
onChange?: (fileList: any[]) => void; onChange?: (fileList: any[]) => void;
fileList?: any[]; fileList?: any[];
request: ProTableProps<any, any>['request'];
toolBarRender?: ProTableProps<any, any>['toolBarRender'];
actionRef?: MutableRefObject<ActionType | undefined>;
}; };
} }

View File

@ -168,6 +168,7 @@ export default function MaterialCategoryList(
}} }}
tree={{ tree={{
fieldProps: { fieldProps: {
bordered: true,
ghost, ghost,
//@ts-ignore //@ts-ignore
search, search,

View File

@ -0,0 +1,130 @@
import { business } from '@/services';
import { FolderOpenOutlined, FolderOutlined } from '@ant-design/icons';
import { ProCard } from '@ant-design/pro-components';
import type { MenuItemProps } from 'antd';
import { Empty, Menu, message } from 'antd';
import React, { useEffect, useState } from 'react';
import useStyle from './style.style';
interface CategoryTreeProps {
onCategorySelect: (categoryId?: string, categoryName?: string) => void;
selectedCategoryId?: string;
}
/**
* 使 Menu
* /
*/
const CategoryTree: React.FC<CategoryTreeProps> = ({
onCategorySelect,
selectedCategoryId,
}) => {
const { styles } = useStyle();
const [treeData, setTreeData] = useState<BusinessAPI.CategoryVO[]>([]);
const [loading, setLoading] = useState(false);
// 获取分类树数据
const fetchCategoryTree = async () => {
setLoading(true);
try {
const { data } = await business.materialCategory.treeMaterialCategory({
categoryTreeQry: {},
});
if (data) {
setTreeData(data as BusinessAPI.CategoryVO[]);
}
} catch (error) {
console.error('获取素材分类树失败:', error);
message.error('获取素材分类树失败');
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchCategoryTree();
}, []);
// 转换数据为 Menu Item 格式
const convertToMenuItems = (nodes: BusinessAPI.CategoryVO[]): any[] => {
return nodes.map((node) => {
const menuItem: any = {
key: node.categoryId,
label: (
<div className={styles.menuItemLabel}>
<span className={styles.categoryName}>{node.name}</span>
<span className={styles.materialCount}>
{/* @ts-ignore */}({node.materialCount || 0})
</span>
</div>
),
icon:
node.children && node.children.length > 0 ? (
<FolderOpenOutlined />
) : (
<FolderOutlined />
),
};
// 递归处理子节点
if (node.children && node.children.length > 0) {
menuItem.children = convertToMenuItems(node.children);
}
return menuItem;
});
};
// 处理菜单选择
const handleMenuSelect: MenuItemProps['onClick'] = ({ key }) => {
const categoryId = key as string;
// 从树数据中查找分类名称
const findCategoryName = (
nodes: BusinessAPI.CategoryVO[],
targetId: string,
): string | undefined => {
for (const node of nodes) {
if (node.categoryId === targetId) {
return node.name;
}
if (node.children) {
const found = findCategoryName(node.children, targetId);
if (found) return found;
}
}
return undefined;
};
const categoryName = findCategoryName(treeData, categoryId);
onCategorySelect(categoryId, categoryName);
};
const menuItems = convertToMenuItems(treeData);
return (
<ProCard
title={
<div className={styles.cardHeader}>
<span></span>
</div>
}
size="small"
className={styles.categoryCard}
loading={loading}
>
{menuItems.length > 0 ? (
<Menu
mode="inline"
selectedKeys={selectedCategoryId ? [selectedCategoryId] : []}
onClick={handleMenuSelect}
items={menuItems}
className={styles.categoryMenu}
/>
) : (
<Empty description="暂无分类" image={Empty.PRESENTED_IMAGE_SIMPLE} />
)}
</ProCard>
);
};
export default CategoryTree;

View File

@ -0,0 +1,336 @@
import { CategoryTree, MaterialList } from '@/components';
import { business } from '@/services';
import { formatParam } from '@/utils/formatParam';
import { pagination } from '@/utils/pagination';
import {
ActionType,
ProColumns,
ProTable,
ProTableProps,
} from '@ant-design/pro-components';
import { Button, Image, message, Modal, ModalProps, Segmented } from 'antd';
import React, { useRef, useState } from 'react';
import useStyle from './style.style';
export interface MaterialLibraryModalProps extends ModalProps {
/** 最大选择数量,默认 1 */
maxCount?: number;
/** 选择模式: radio-单选, checkbox-多选 */
mode?: 'radio' | 'checkbox';
/** 确认选择回调 */
onFinish?: (materials: BusinessAPI.MaterialVO[]) => void;
/** 初始已选中的素材列表 */
initialSelectedMaterials?: BusinessAPI.MaterialVO[];
/** 素材类型筛选: FILE_IMAGE-图片, FILE_VIDEO-视频 */
materialType?: 'FILE_IMAGE' | 'FILE_VIDEO';
}
const MaterialLibraryModal: React.FC<MaterialLibraryModalProps> = ({
open,
onCancel,
maxCount = 1,
mode = 'radio',
onFinish,
initialSelectedMaterials = [],
materialType,
...rest
}) => {
const { styles } = useStyle();
const [selectedCategoryId, setSelectedCategoryId] = useState<
string | undefined
>();
const [selectedCategoryName, setSelectedCategoryName] = useState<
string | undefined
>();
const [selectedMaterials, setSelectedMaterials] = useState<
BusinessAPI.MaterialVO[]
>(initialSelectedMaterials);
const [viewMode, setViewMode] = useState<'list' | 'image'>('image');
const [materials, setMaterials] = useState<BusinessAPI.MaterialVO[]>([]);
const actionRef = useRef<ActionType>();
// 列表模式列配置
const columns: ProColumns<BusinessAPI.MaterialVO>[] = [
{
title: '素材名称',
dataIndex: 'name',
ellipsis: true,
},
{
title: '素材内容',
dataIndex: 'url',
render: (_, record) => {
if (record.type === 'FILE_IMAGE' || record.type === 'FILE_VIDEO') {
return (
<Image
width={40}
height={40}
src={record.url}
style={{ objectFit: 'cover', borderRadius: 4 }}
preview={{ src: record.url }}
/>
);
}
return '-';
},
},
{
title: '素材分类',
dataIndex: ['categoryVO', 'name'],
},
{
title: '创建时间',
dataIndex: 'createdAt',
valueType: 'dateTime',
},
];
// 处理分类选择
const handleCategorySelect = (categoryId?: string, categoryName?: string) => {
setSelectedCategoryId(categoryId);
setSelectedCategoryName(categoryName);
// 刷新素材列表
actionRef.current?.reload();
};
// 处理素材选择
const handleMaterialSelect = (material: BusinessAPI.MaterialVO) => {
const isSelected = selectedMaterials.some(
(m) => m.materialId === material.materialId,
);
if (isSelected) {
// 取消选择
setSelectedMaterials((prev) =>
prev.filter((m) => m.materialId !== material.materialId),
);
} else {
// 选择素材
if (mode === 'radio') {
// 单选模式直接替换
setSelectedMaterials([material]);
} else {
// 多选模式检查数量限制
if (maxCount > 0 && selectedMaterials.length >= maxCount) {
message.warning(`最多只能选择 ${maxCount} 个素材`);
return;
}
setSelectedMaterials((prev) => [...prev, material]);
}
}
};
// 确认选择
const handleOk = () => {
if (selectedMaterials.length === 0 && maxCount > 0) {
message.warning('请先选择素材');
return;
}
onFinish?.(selectedMaterials);
// 重置状态
setSelectedMaterials([]);
setSelectedCategoryId(undefined);
setSelectedCategoryName(undefined);
};
// 关闭弹窗
const handleClose = () => {
setSelectedMaterials([]);
setSelectedCategoryId(undefined);
setSelectedCategoryName(undefined);
onCancel?.({} as any);
};
// 图片预览渲染
const renderImagePreview = () => {
if (materials.length === 0) {
return (
<div className={styles.emptyTip}>
<span></span>
</div>
);
}
return (
<div className={styles.imagePreview}>
{materials.map((material) => {
const isSelected = selectedMaterials.some(
(m) => m.materialId === material.materialId,
);
return (
<div
key={material.materialId}
className={`${styles.imageItem} ${isSelected ? styles.imageItemSelected : ''}`}
onClick={() => handleMaterialSelect(material)}
>
<div className={styles.imageWrapper}>
<Image
src={material.url}
alt={material.name}
preview={{ src: material.url }}
placeholder={
<div className={styles.imagePlaceholder}>...</div>
}
/>
</div>
<div className={styles.imageInfo}>
<div className={styles.imageName}>{material.name}</div>
</div>
{isSelected && (
<div className={styles.imageSelectedCheck}>
{mode === 'radio'
? '✓'
: `${
selectedMaterials.findIndex(
(m) => m.materialId === material.materialId,
) + 1
}`}
</div>
)}
</div>
);
})}
</div>
);
};
// 请求参数
const request: ProTableProps<
BusinessAPI.MaterialVO,
BusinessAPI.MaterialPageQry
>['request'] = async (params, sorter, filter) => {
const searchParams: BusinessAPI.MaterialPageQry = {
...formatParam<typeof params>(params, sorter, filter),
categoryId: selectedCategoryId,
type: materialType,
};
const { data, success, totalCount } = await business.material.pageMaterial({
materialPageQry: searchParams,
});
// 保存素材数据用于图片预览
if (data) {
setMaterials(data as BusinessAPI.MaterialVO[]);
}
return {
data: data || [],
total: totalCount,
success,
};
};
return (
<Modal
title="素材库"
open={open}
onOk={handleOk}
onCancel={handleClose}
width={1200}
destroyOnHidden
{...rest}
className={styles.materialModal}
>
<ProTable<BusinessAPI.MaterialVO, BusinessAPI.MaterialPageQry>
actionRef={actionRef}
rowKey="materialId"
bordered={true}
columns={columns}
request={request}
search={false}
pagination={pagination()}
size="small"
className={styles.materialTable}
rowClassName={(record) => {
const isSelected = selectedMaterials.some(
(m) => m.materialId === record.materialId,
);
return isSelected ? styles.rowSelected : '';
}}
onRow={(record) => ({
onClick: () => handleMaterialSelect(record),
style: { cursor: 'pointer' },
})}
toolBarRender={false}
tableRender={(props, defaultDom, domList) => {
return (
<div className={styles.modalContent}>
{/* 左侧分类树 */}
<div className={styles.leftPanel}>
<CategoryTree
onCategorySelect={handleCategorySelect}
selectedCategoryId={selectedCategoryId}
/>
</div>
{/* 右侧素材内容 */}
<div className={styles.rightPanel}>
{/* 顶部工具栏 */}
<div className={styles.toolbar}>
<div className={styles.toolbarLeft}>
<Segmented
value={viewMode}
onChange={(value) =>
setViewMode(value as 'list' | 'image')
}
options={[
{ label: '图片', value: 'image' },
{ label: '列表', value: 'list' },
]}
/>
{selectedCategoryName && (
<span className={styles.currentCategory}>
: <strong>{selectedCategoryName}</strong>
</span>
)}
</div>
<div className={styles.toolbarRight}>
<MaterialList
formType={'modal'}
key={'create'}
ghost={true}
mode={'create'}
search={false}
onValueChange={() => actionRef.current?.reload()}
/>
{selectedMaterials.length > 0 && (
<span className={styles.selectedCount}>
{selectedMaterials.length}
{maxCount > 0 && `/${maxCount}`}
</span>
)}
{selectedMaterials.length > 0 && mode === 'checkbox' && (
<Button
size="small"
onClick={() => setSelectedMaterials([])}
>
</Button>
)}
</div>
</div>
{/* 素材展示区域 */}
<div className={styles.materialArea}>
{viewMode === 'list' ? (
defaultDom
) : (
<div className={styles.imageGridWrapper}>
{renderImagePreview()}
<div className={styles.paginationWrapper}>
{domList.table}
</div>
</div>
)}
</div>
</div>
</div>
);
}}
/>
</Modal>
);
};
export default MaterialLibraryModal;

View File

@ -0,0 +1,3 @@
export { default as CategoryTree } from './CategoryTree';
export { default as MaterialLibraryModal } from './MaterialLibraryModal';
export type { MaterialLibraryModalProps } from './MaterialLibraryModal';

View File

@ -0,0 +1,224 @@
import { createStyles } from 'antd-style';
const useStyle = () => {
return createStyles(() => {
return {
// ==================== Modal 布局 ====================
materialModal: {
'.ant-modal-body': {
padding: 0,
},
},
modalContent: {
display: 'flex',
height: 600,
},
leftPanel: {
width: 280,
borderRight: '1px solid #f0f0f0',
padding: 16,
overflow: 'auto',
},
rightPanel: {
flex: 1,
display: 'flex',
flexDirection: 'column',
overflow: 'hidden',
},
// ==================== 工具栏 ====================
toolbar: {
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
padding: '12px 16px',
borderBottom: '1px solid #f0f0f0',
},
toolbarLeft: {
display: 'flex',
alignItems: 'center',
gap: 16,
},
toolbarRight: {
display: 'flex',
alignItems: 'center',
gap: 12,
},
currentCategory: {
color: '#666',
fontSize: 13,
},
selectedCount: {
color: '#1890ff',
fontWeight: 500,
},
// ==================== 分类菜单 ====================
categoryCard: {
height: '100%',
},
cardHeader: {
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
},
categoryMenu: {
borderRight: 'none',
'.ant-menu-item': {
paddingLeft: '8px !important',
height: 'auto',
lineHeight: 'normal',
paddingTop: 8,
paddingBottom: 8,
margin: 0,
},
'.ant-menu-submenu': {
paddingLeft: '8px !important',
},
'.ant-menu-submenu-title': {
paddingLeft: '8px !important',
height: 'auto',
lineHeight: 'normal',
paddingTop: 8,
paddingBottom: 8,
margin: 0,
},
'.ant-menu-item-selected': {
backgroundColor: '#e6f7ff',
},
'.ant-menu-item:hover': {
backgroundColor: 'rgba(0, 0, 0, 4%)',
},
'.ant-menu-submenu-title:hover': {
backgroundColor: 'rgba(0, 0, 0, 4%)',
},
},
menuItemLabel: {
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
flex: 1,
marginLeft: 8,
},
categoryName: {
flex: 1,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
},
materialCount: {
color: '#999',
fontSize: 12,
flexShrink: 0,
marginLeft: 8,
},
// ==================== 素材列表 ====================
materialArea: {
flex: 1,
overflow: 'auto',
padding: 16,
},
materialTable: {
'.ant-table-row': {
cursor: 'pointer',
},
border: '1px solid #f0f0f0;',
borderRadius: 8,
},
rowSelected: {
backgroundColor: '#e6f7ff !important',
},
// ==================== 图片预览 ====================
imageGridWrapper: {
height: '100%',
display: 'flex',
flexDirection: 'column',
},
imagePreview: {
display: 'flex',
flexWrap: 'wrap',
gap: 12,
overflow: 'auto',
padding: '4px 0',
// flex: 1,
},
imageItem: {
width: 120,
border: '1px solid #f0f0f0',
borderRadius: 8,
overflow: 'hidden',
cursor: 'pointer',
transition: 'all 0.2s',
position: 'relative',
'&:hover': {
borderColor: '#1890ff',
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
},
},
imageItemSelected: {
borderColor: '#1890ff',
boxShadow: '0 0 0 2px rgba(24, 144, 255, 0.3)',
},
imageWrapper: {
width: '100%',
height: 100,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#fafafa',
img: {
maxWidth: '100%',
maxHeight: '100%',
objectFit: 'contain',
},
},
imagePlaceholder: {
color: '#999',
fontSize: 12,
},
imageInfo: {
padding: '8px 10px',
backgroundColor: '#fff',
},
imageName: {
fontSize: 12,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
color: '#333',
},
imageSelectedCheck: {
position: 'absolute',
top: 4,
right: 4,
width: 20,
height: 20,
borderRadius: '50%',
backgroundColor: '#1890ff',
color: '#fff',
fontSize: 12,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
},
paginationWrapper: {
borderTop: '1px solid #f0f0f0',
marginTop: 8,
'.ant-table': {
display: 'none',
},
},
emptyTip: {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
height: '100%',
color: '#999',
},
};
})();
};
export default useStyle;

View File

@ -1,6 +1,7 @@
import { import {
BizContainer, BizContainer,
BizValueType, BizValueType,
FormType,
MaterialCategoryList, MaterialCategoryList,
ModeType, ModeType,
ProFormBizTreeSelect, ProFormBizTreeSelect,
@ -27,6 +28,7 @@ export interface IMaterialListProps {
mode?: ModeType; mode?: ModeType;
trigger?: () => React.ReactNode; trigger?: () => React.ReactNode;
onValueChange?: () => void; onValueChange?: () => void;
formType: FormType;
} }
export default function MaterialList(props: IMaterialListProps) { export default function MaterialList(props: IMaterialListProps) {
@ -36,6 +38,7 @@ export default function MaterialList(props: IMaterialListProps) {
search = true, search = true,
onValueChange, onValueChange,
trigger, trigger,
formType = 'drawer',
} = props; } = props;
const intl = useIntl(); const intl = useIntl();
const intlPrefix = 'material'; const intlPrefix = 'material';
@ -291,7 +294,7 @@ export default function MaterialList(props: IMaterialListProps) {
columns, columns,
}} }}
create={{ create={{
formType: 'drawer', formType: formType,
formContext, formContext,
trigger, trigger,
request: async ( request: async (

View File

@ -1,3 +1,4 @@
export { default as MaterialCategoryList } from './MaterialCategoryList'; export { default as MaterialCategoryList } from './MaterialCategoryList';
export * from './MaterialLibrary';
export { default as MaterialList } from './MaterialList'; export { default as MaterialList } from './MaterialList';
export { default as MaterialModal } from './MaterialModal'; export { default as MaterialModal } from './MaterialModal';

View File

@ -3,7 +3,6 @@ import {
ButtonAccess, ButtonAccess,
CompanyPaymentAccountSelect, CompanyPaymentAccountSelect,
InsertPosition, InsertPosition,
MaterialList,
OrderSupplierInvoiceList, OrderSupplierInvoiceList,
ProFormUploadMaterial, ProFormUploadMaterial,
SupplierFarmerList, SupplierFarmerList,
@ -12,11 +11,9 @@ import {
} from '@/components'; } from '@/components';
import { business } from '@/services'; import { business } from '@/services';
import { formatCurrency } from '@/utils/format'; import { formatCurrency } from '@/utils/format';
import { formatParam } from '@/utils/formatParam';
import { formLayout } from '@/utils/formLayout'; import { formLayout } from '@/utils/formLayout';
import { useIntl } from '@@/exports'; import { useIntl } from '@@/exports';
import { import {
ActionType,
DrawerForm, DrawerForm,
ProCard, ProCard,
ProFormDependency, ProFormDependency,
@ -26,7 +23,6 @@ import {
RouteContextType, RouteContextType,
} from '@ant-design/pro-components'; } from '@ant-design/pro-components';
import { Col, Row, Space, Table } from 'antd'; import { Col, Row, Space, Table } from 'antd';
import { useRef } from 'react';
export interface IPaymentTaskPayProps { export interface IPaymentTaskPayProps {
insertPosition?: InsertPosition; insertPosition?: InsertPosition;
@ -43,8 +39,6 @@ export default function PaymentTaskPay(props: IPaymentTaskPayProps) {
const unpaidAmount = const unpaidAmount =
(paymentTaskVO.totalAmount || 0) - (paymentTaskVO.paidAmount || 0); (paymentTaskVO.totalAmount || 0) - (paymentTaskVO.paidAmount || 0);
const actionRef = useRef<ActionType>();
const handleSubmit = async (formData: any) => { const handleSubmit = async (formData: any) => {
console.log('付款数据:', formData); console.log('付款数据:', formData);
// TODO: 调用付款接口 // TODO: 调用付款接口
@ -370,33 +364,7 @@ export default function PaymentTaskPay(props: IPaymentTaskPayProps) {
}; };
}} }}
fieldProps={{ fieldProps={{
maxCount: 1, maxCount: 9,
actionRef: actionRef,
toolBarRender: () => [
<MaterialList
key={'create'}
ghost={true}
mode={'create'}
search={false}
onValueChange={() => actionRef.current?.reload()}
/>,
],
request: async (params, sorter, filter) => {
const { data, success, totalCount } =
await business.material.pageMaterial({
materialPageQry: formatParam<typeof params>(
params,
sorter,
filter,
),
});
return {
data: data || [],
total: totalCount,
success,
};
},
}} }}
/> />

View File

@ -1,11 +1,5 @@
import { import { BizEditor, ButtonAccess, ProFormUploadMaterial } from '@/components';
BizEditor,
ButtonAccess,
MaterialList,
ProFormUploadMaterial,
} from '@/components';
import { business } from '@/services'; import { business } from '@/services';
import { formatParam } from '@/utils/formatParam';
import { formLayout } from '@/utils/formLayout'; import { formLayout } from '@/utils/formLayout';
import { import {
DrawerForm, DrawerForm,
@ -150,30 +144,6 @@ export default function ChargingPilePurchaseConfig(
} }
fieldProps={{ fieldProps={{
maxCount: 1, maxCount: 1,
toolBarRender: () => [
<MaterialList
key={'create'}
ghost={true}
mode={'create'}
search={false}
/>,
],
request: async (params, sorter, filter) => {
const { data, success, totalCount } =
await business.material.pageMaterial({
materialPageQry: formatParam<typeof params>(
params,
sorter,
filter,
),
});
return {
data: data || [],
total: totalCount,
success,
};
},
}} }}
/> />
<ProFormDependency key={'content'} name={['content']}> <ProFormDependency key={'content'} name={['content']}>

View File

@ -1,18 +1,14 @@
import { MaterialList, ProFormUploadMaterial } from '@/components'; import { ProFormUploadMaterial } from '@/components';
import { business } from '@/services'; import { business } from '@/services';
import { formatParam } from '@/utils/formatParam';
import { formLayout } from '@/utils/formLayout'; import { formLayout } from '@/utils/formLayout';
import { import {
ActionType,
ProForm, ProForm,
ProFormText, ProFormText,
ProFormUploadDragger, ProFormUploadDragger,
} from '@ant-design/pro-components'; } from '@ant-design/pro-components';
import { Col, message, Row, Space } from 'antd'; import { Col, message, Row, Space } from 'antd';
import { useRef } from 'react';
export default function WxMaConfig() { export default function WxMaConfig() {
const actionRef = useRef<ActionType>();
return ( return (
<ProForm<BusinessAPI.WxMaConfigValue> <ProForm<BusinessAPI.WxMaConfigValue>
{...formLayout()} {...formLayout()}
@ -113,32 +109,6 @@ export default function WxMaConfig() {
}} }}
fieldProps={{ fieldProps={{
maxCount: 1, maxCount: 1,
actionRef: actionRef,
toolBarRender: () => [
<MaterialList
key={'create'}
ghost={true}
mode={'create'}
search={false}
onValueChange={() => actionRef.current?.reload()}
/>,
],
request: async (params, sorter, filter) => {
const { data, success, totalCount } =
await business.material.pageMaterial({
materialPageQry: formatParam<typeof params>(
params,
sorter,
filter,
),
});
return {
data: data || [],
total: totalCount,
success,
};
},
}} }}
/> />
<ProFormUploadDragger <ProFormUploadDragger

View File

@ -1,13 +1,10 @@
import { MaterialList, ProFormUploadMaterial } from '@/components'; import { ProFormUploadMaterial } from '@/components';
import { business } from '@/services'; import { business } from '@/services';
import { formatParam } from '@/utils/formatParam';
import { formLayout } from '@/utils/formLayout'; import { formLayout } from '@/utils/formLayout';
import { ActionType, ProForm, ProFormText } from '@ant-design/pro-components'; import { ProForm, ProFormText } from '@ant-design/pro-components';
import { Col, message, Row, Space } from 'antd'; import { Col, message, Row, Space } from 'antd';
import { useRef } from 'react';
export default function WxMaConfig() { export default function WxMaConfig() {
const actionRef = useRef<ActionType>();
return ( return (
<ProForm<BusinessAPI.WxMaConfigValue> <ProForm<BusinessAPI.WxMaConfigValue>
{...formLayout()} {...formLayout()}
@ -108,32 +105,6 @@ export default function WxMaConfig() {
}} }}
fieldProps={{ fieldProps={{
maxCount: 1, maxCount: 1,
actionRef: actionRef,
toolBarRender: () => [
<MaterialList
key={'create'}
ghost={true}
mode={'create'}
search={false}
onValueChange={() => actionRef.current?.reload()}
/>,
],
request: async (params, sorter, filter) => {
const { data, success, totalCount } =
await business.material.pageMaterial({
materialPageQry: formatParam<typeof params>(
params,
sorter,
filter,
),
});
return {
data: data || [],
total: totalCount,
success,
};
},
}} }}
/> />
</ProForm> </ProForm>

View File

@ -1,22 +1,16 @@
import { import {
BizContainer, BizContainer,
BizValueType, BizValueType,
MaterialList,
ModeType, ModeType,
ProFormUploadMaterial, ProFormUploadMaterial,
} from '@/components'; } from '@/components';
import { business } from '@/services'; import { business } from '@/services';
import { formatBankCard, formatIdCard, formatPhone } from '@/utils/format'; import { formatBankCard, formatIdCard, formatPhone } from '@/utils/format';
import { formatParam } from '@/utils/formatParam';
import { useIntl } from '@@/exports'; import { useIntl } from '@@/exports';
import { EyeInvisibleOutlined, EyeTwoTone } from '@ant-design/icons'; import { EyeInvisibleOutlined, EyeTwoTone } from '@ant-design/icons';
import { import { ProColumns, ProFormText } from '@ant-design/pro-components';
ActionType,
ProColumns,
ProFormText,
} from '@ant-design/pro-components';
import { ProDescriptionsItemProps } from '@ant-design/pro-descriptions'; import { ProDescriptionsItemProps } from '@ant-design/pro-descriptions';
import React, { useRef, useState } from 'react'; import React, { useState } from 'react';
interface ISupplierFarmerListProps { interface ISupplierFarmerListProps {
ghost?: boolean; ghost?: boolean;
@ -38,7 +32,6 @@ export default function SupplierFarmerList(props: ISupplierFarmerListProps) {
} = props; } = props;
const intl = useIntl(); const intl = useIntl();
const intlPrefix = 'supplierFarmer'; const intlPrefix = 'supplierFarmer';
const actionRef = useRef<ActionType>();
const [showIdCard, setShowIdCard] = useState<Record<string, boolean>>({}); const [showIdCard, setShowIdCard] = useState<Record<string, boolean>>({});
const [showBankCard, setShowBankCard] = useState<Record<string, boolean>>({}); const [showBankCard, setShowBankCard] = useState<Record<string, boolean>>({});
@ -255,32 +248,6 @@ export default function SupplierFarmerList(props: ISupplierFarmerListProps) {
}} }}
fieldProps={{ fieldProps={{
maxCount: 1, maxCount: 1,
actionRef: actionRef,
toolBarRender: () => [
<MaterialList
key={'create'}
ghost={true}
mode={'create'}
search={false}
onValueChange={() => actionRef.current?.reload()}
/>,
],
request: async (params, sorter, filter) => {
const { data, success, totalCount } =
await business.material.pageMaterial({
materialPageQry: formatParam<typeof params>(
params,
sorter,
filter,
),
});
return {
data: data || [],
total: totalCount,
success,
};
},
}} }}
/>, />,
]; ];

View File

@ -1,3 +1,4 @@
import { SelectModal } from '@/components';
import { business } from '@/services'; import { business } from '@/services';
import { formatBankCard, formatIdCard, formatPhone } from '@/utils/format'; import { formatBankCard, formatIdCard, formatPhone } from '@/utils/format';
import { formatParam } from '@/utils/formatParam'; import { formatParam } from '@/utils/formatParam';
@ -10,7 +11,6 @@ import {
ProColumns, ProColumns,
ProFormText, ProFormText,
} from '@ant-design/pro-components'; } from '@ant-design/pro-components';
import { SelectModal } from '@/components';
import { Alert, ModalProps, Row, Tag } from 'antd'; import { Alert, ModalProps, Row, Tag } from 'antd';
import React, { useEffect, useRef, useState } from 'react'; import React, { useEffect, useRef, useState } from 'react';
@ -159,13 +159,13 @@ export default function SupplierModal(props: ISupplierModalProps) {
</div> </div>
), ),
}, },
{ // {
title: intl.formatMessage({ id: intlPrefix + '.column.wechatQr' }), // title: intl.formatMessage({ id: intlPrefix + '.column.wechatQr' }),
dataIndex: 'wechatQr', // dataIndex: 'wechatQr',
valueType: 'image', // valueType: 'image',
key: 'wechatQr', // key: 'wechatQr',
search: false, // search: false,
}, // },
...(initExtraColumns || []), ...(initExtraColumns || []),
]; ];

View File

@ -1,22 +1,16 @@
import { import {
BizContainer, BizContainer,
BizValueType, BizValueType,
MaterialList,
ModeType, ModeType,
ProFormUploadMaterial, ProFormUploadMaterial,
} from '@/components'; } from '@/components';
import { business } from '@/services'; import { business } from '@/services';
import { formatBankCard, formatPhone } from '@/utils/format'; import { formatBankCard, formatPhone } from '@/utils/format';
import { formatParam } from '@/utils/formatParam';
import { useIntl } from '@@/exports'; import { useIntl } from '@@/exports';
import { EyeInvisibleOutlined, EyeTwoTone } from '@ant-design/icons'; import { EyeInvisibleOutlined, EyeTwoTone } from '@ant-design/icons';
import { import { ProColumns, ProFormText } from '@ant-design/pro-components';
ActionType,
ProColumns,
ProFormText,
} from '@ant-design/pro-components';
import { ProDescriptionsItemProps } from '@ant-design/pro-descriptions'; import { ProDescriptionsItemProps } from '@ant-design/pro-descriptions';
import React, { useRef, useState } from 'react'; import React, { useState } from 'react';
interface ISupplierStallListProps { interface ISupplierStallListProps {
ghost?: boolean; ghost?: boolean;
@ -38,7 +32,6 @@ export default function SupplierStallList(props: ISupplierStallListProps) {
} = props; } = props;
const intl = useIntl(); const intl = useIntl();
const intlPrefix = 'supplierStall'; const intlPrefix = 'supplierStall';
const actionRef = useRef<ActionType>();
const [showBankCard, setShowBankCard] = useState<Record<string, boolean>>({}); const [showBankCard, setShowBankCard] = useState<Record<string, boolean>>({});
const [showPhone, setShowPhone] = useState<Record<string, boolean>>({}); const [showPhone, setShowPhone] = useState<Record<string, boolean>>({});
@ -214,32 +207,6 @@ export default function SupplierStallList(props: ISupplierStallListProps) {
}} }}
fieldProps={{ fieldProps={{
maxCount: 1, maxCount: 1,
actionRef: actionRef,
toolBarRender: () => [
<MaterialList
key={'create'}
ghost={true}
mode={'create'}
search={false}
onValueChange={() => actionRef.current?.reload()}
/>,
],
request: async (params, sorter, filter) => {
const { data, success, totalCount } =
await business.material.pageMaterial({
materialPageQry: formatParam<typeof params>(
params,
sorter,
filter,
),
});
return {
data: data || [],
total: totalCount,
success,
};
},
}} }}
/>, />,
]; ];

View File

@ -1,36 +1,41 @@
import { MaterialLibraryModal } from '@/components';
import { UploadOutlined } from '@ant-design/icons'; import { UploadOutlined } from '@ant-design/icons';
import { ActionType, ProTableProps } from '@ant-design/pro-components';
import { SelectModal } from '@/components';
import { Upload } from 'antd'; import { Upload } from 'antd';
import React, { MutableRefObject, useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import useStyle from './style.style'; import useStyle from './style.style';
export interface UploadMaterialProps { export interface UploadMaterialProps {
maxCount: number; maxCount: number;
onChange: (fileList: any[]) => void; onChange: (fileList: any[]) => void;
fileList?: any[]; fileList?: any[];
request: ProTableProps<any, any>['request'];
toolBarRender?: ProTableProps<any, any>['toolBarRender'];
actionRef?: MutableRefObject<ActionType | undefined>;
} }
const UploadMaterial: React.FC<UploadMaterialProps> = (props) => { const UploadMaterial: React.FC<UploadMaterialProps> = (props) => {
const { const { maxCount, onChange, fileList: initialFileList } = props;
maxCount,
fileList: initialFileList,
onChange,
request,
toolBarRender,
actionRef,
} = props;
const [fileList, setFileList] = useState<any[]>([]); const [fileList, setFileList] = useState<any[]>([]);
const { styles } = useStyle();
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const { styles } = useStyle();
useEffect(() => { useEffect(() => {
setFileList(initialFileList || []); setFileList(initialFileList || []);
}, [initialFileList]); }, [initialFileList]);
// 处理素材选择完成
const handleMaterialSelect = (materials: BusinessAPI.MaterialVO[]) => {
const imageList = materials.map((materialVO) => ({
uid: materialVO?.materialId,
name: materialVO?.name,
// @ts-ignore
status: 'success',
thumbUrl: materialVO?.url,
url: materialVO?.url,
}));
const newFileList = [...fileList, ...imageList];
setFileList(newFileList);
onChange(newFileList);
};
return ( return (
<> <>
<Upload <Upload
@ -58,71 +63,20 @@ const UploadMaterial: React.FC<UploadMaterialProps> = (props) => {
)} )}
</Upload> </Upload>
{/* 选择图片 */} {/* 素材库选择弹窗 */}
<SelectModal <MaterialLibraryModal
rowKey={'materialId'} open={open}
modalProps={{ onCancel={() => setOpen(false)}
title: '选择图片', maxCount={maxCount - fileList.length}
open: open, mode="checkbox"
onOk: () => setOpen(false), materialType="FILE_IMAGE"
onCancel: () => setOpen(false), onFinish={(materials) => {
handleMaterialSelect(materials);
setOpen(false);
}} }}
tableProps={{
actionRef: actionRef,
toolBarRender: toolBarRender,
rowKey: 'materialId',
columns: [
{
title: '素材内容',
dataIndex: 'url',
valueType: 'image',
},
{
title: '素材名称',
dataIndex: 'name',
},
{
title: '素材分类',
dataIndex: ['categoryVO', 'name'],
},
],
params: {
materialPageQry: {
type: 'FILE_IMAGE',
},
},
request: request,
pagination: {
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total: number) => `${total}`,
pageSizeOptions: ['10', '20', '50', '100'],
defaultPageSize: 10,
hideOnSinglePage: true,
position: ['bottomRight'],
},
}}
onFinish={(materialList) => {
const imageList = materialList.map((materialVO) => {
return {
uid: materialVO?.materialId,
name: materialVO?.name,
// @ts-ignore
status: 'success',
thumbUrl: materialVO?.url,
url: materialVO?.url,
};
});
const newFileList = [...fileList, ...imageList];
setFileList(newFileList);
onChange(newFileList);
}}
num={(maxCount || 1) - fileList.length}
type={'checkbox'}
/> />
</> </>
); );
}; };
export default UploadMaterial; export default UploadMaterial;

View File

@ -1,30 +1,29 @@
import { createStyles } from 'antd-style'; import { createStyles } from 'antd-style';
const useStyle = () => { const useStyle = () => {
return createStyles(() => { return createStyles(() => {
return { return {
uploadImage: { uploadImage: {
width: 102, width: 102,
height: 102, height: 102,
marginInlineEnd: 0, marginInlineEnd: 0,
marginBottom: 0, marginBottom: 0,
textAlign: 'center', textAlign: 'center',
verticalAlign: 'top', verticalAlign: 'top',
backgroundColor: "rgba(0, 0, 0, 2%)", backgroundColor: 'rgba(0, 0, 0, 2%)',
//border: 1px dashed #d9d9d9, borderRadius: 8,
borderRadius: 8, cursor: 'pointer',
cursor: 'pointer', transition: 'border-color 0.3s',
transition: 'border-color 0.3s', },
}, uploadImageIcon: {
uploadImageIcon: { display: 'flex',
display: 'flex', alignItems: 'center',
alignItems: 'center', justifyContent: 'center',
justifyContent: 'center', height: '100%',
height: '100%', textAlign: 'center',
textAlign: 'center', },
}, };
}; })();
})(); };
}
export default useStyle; export default useStyle;