forked from yoone/WEB
1
0
Fork 0

feat(产品): 添加产品分组功能页面

实现产品按属性分组展示功能,支持按分类和属性筛选
This commit is contained in:
tikkhun 2026-01-20 17:19:02 +08:00
parent d2a84a9a4a
commit 2ee8964bce
2 changed files with 370 additions and 2 deletions

View File

@ -166,8 +166,8 @@ export default defineConfig({
}, },
{ {
name: '产品品牌空间', name: '产品品牌空间',
path: '/product/brandspace', path: '/product/groupBy',
component: './Product/BrandSpace', component: './Product/GroupBy',
}, },
// sync // sync
{ {

View File

@ -0,0 +1,368 @@
import React, { useEffect, useState, useMemo } from 'react';
import { PageContainer, ProFromSelect } from '@ant-design/pro-components';
import { Card, Collapse, Divider, Image, Select, Space, Typography, message } from 'antd';
import { categorycontrollerGetall } from '@/servers/api/category';
import { productcontrollerGetproductlistgrouped } from '@/servers/api/product';
import { dictcontrollerGetdictitems } from '@/servers/api/dict';
// Define interfaces
interface Category {
id: number;
name: string;
title: string;
attributes: string[]; // List of attribute names for this category
}
interface Attribute {
id: number;
name: string;
title: string;
}
interface AttributeValue {
id: number;
name: string;
title: string;
titleCN?: string;
value?: string;
image?: string;
}
interface Product {
id: number;
sku: string;
name: string;
image?: string;
brandId: number;
brandName: string;
attributes: { [key: string]: number }; // attribute name to attribute value id mapping
price?: number;
}
// Grouped products by attribute value
interface GroupedProducts {
[attributeValueId: string]: Product[];
}
// ProductCard component for displaying single product
const ProductCard: React.FC<{ product: Product }> = ({ product }) => {
return (
<Card hoverable style={{ width: 240 }}>
{/* <div style={{ height: 180, overflow: 'hidden', marginBottom: '12px' }}>
<Image
src={product.image || 'https://via.placeholder.com/240x180?text=No+Image'}
alt={product.name}
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
/>
</div> */}
<div>
<Typography.Text type="secondary" style={{ fontSize: 12, display: 'block', marginBottom: '4px' }}>
{product.sku}
</Typography.Text>
<Typography.Text ellipsis style={{ width: '100%', display: 'block', marginBottom: '8px' }}>
{product.name}
</Typography.Text>
<Typography.Text strong style={{ fontSize: 16, color: '#ff4d4f', display: 'block' }}>
¥{product.price || '--'}
</Typography.Text>
</div>
</Card>
);
};
// ProductGroup component for displaying grouped products
const ProductGroup: React.FC<{
attributeValueId: string;
groupProducts: Product[];
attributeValue: AttributeValue | undefined;
attributeName: string;
}> = ({ attributeValueId, groupProducts, attributeValue }) => {
// State for collapse control
const [isCollapsed, setIsCollapsed] = useState(false);
// Create collapse panel header
const panelHeader = (
<Space>
{attributeValue?.image && (
<Image
src={attributeValue.image}
style={{ width: 24, height: 24, objectFit: 'cover', borderRadius: 4 }}
/>
)}
<Typography.Title level={5} style={{ margin: 0 }}>
<span>
{attributeValue?.titleCN || attributeValue?.title || attributeValue?.name || attributeValueId||'未知'}
( {groupProducts.length} )
</span>
</Typography.Title>
</Space>
);
return (
<Collapse
activeKey={isCollapsed ? [] : [attributeValueId]}
onChange={(key) => setIsCollapsed(Array.isArray(key) && key.length === 0)}
ghost
bordered={false}
items={[
{
key: attributeValueId,
label: panelHeader,
children: (
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '16px', paddingTop: '8px' }}>
{groupProducts.map((product) => (
<ProductCard key={product.id} product={product} />
))}
</div>
),
},
]}
/>
);
};
// Main ProductGroupBy component
const ProductGroupBy: React.FC = () => {
// State management
const [categories, setCategories] = useState<Category[]>([]);
const [selectedCategory, setSelectedCategory] = useState<string | null>(null);
// Store selected values for each attribute
const [attributeFilters, setAttributeFilters] = useState<{ [key: string]: number | null }>({});
// Group by attribute
const [groupByAttribute, setGroupByAttribute] = useState<string | null>(null);
// Products
const [products, setProducts] = useState<Product[]>([]);
const [groupedProducts, setGroupedProducts] = useState<GroupedProducts>({});
const [loading, setLoading] = useState(false);
// Extract all unique attributes from categories
const categoryAttributes = useMemo(() => {
if (!selectedCategory) return [];
const categoryItem = categories.find((category: any) => category.name === selectedCategory);
if (!categoryItem) return [];
const attributesList: Attribute[] = categoryItem.attributes.map((attribute: any, index) => ({
...attribute.attributeDict,
id: index + 1,
}));
return attributesList;
}, [selectedCategory]);
// Fetch categories list
const fetchCategories = async () => {
try {
const response = await categorycontrollerGetall();
const rawCategories = Array.isArray(response) ? response : response?.data || [];
setCategories(rawCategories);
// Set default category
if (rawCategories.length > 0) {
const defaultCategory = rawCategories.find((category: any) => category.name === 'nicotine-pouches');
setSelectedCategory(defaultCategory?.name || rawCategories[0].name);
}
} catch (error) {
console.error('Failed to fetch categories:', error);
message.error('获取分类列表失败');
}
};
// Update category attributes when selected category changes
useEffect(() => {
if (!selectedCategory) return;
const category = categories.find(cat => cat.name === selectedCategory);
if (!category) return;
// Get attributes for this category
const attributesForCategory = categoryAttributes.filter(attr =>
attr.name === 'brand' || category.attributes.includes(attr.name)
);
// Reset attribute filters when category changes
const newFilters: { [key: string]: number | null } = {};
attributesForCategory.forEach(attr => {
newFilters[attr.name] = null;
});
setAttributeFilters(newFilters);
// Set default group by attribute
if (attributesForCategory.length > 0) {
setGroupByAttribute(attributesForCategory[0].name);
}
}, [selectedCategory, categories, categoryAttributes]);
// Handle attribute filter change
const handleAttributeFilterChange = (attributeName: string, value: number | null) => {
setAttributeFilters(prev => ({ ...prev, [attributeName]: value }));
};
// Fetch products based on filters
const fetchProducts = async () => {
if (!selectedCategory || !groupByAttribute) return;
setLoading(true);
try {
const params: any = {
category: selectedCategory,
groupBy: groupByAttribute
};
const response = await productcontrollerGetproductlistgrouped(params);
const grouped = response?.data || {};
setGroupedProducts(grouped);
// Flatten grouped products to get all products
const allProducts = Object.values(grouped).flat() as Product[];
setProducts(allProducts);
} catch (error) {
console.error('Failed to fetch grouped products:', error);
message.error('获取分组产品列表失败');
setProducts([]);
setGroupedProducts({});
} finally {
setLoading(false);
}
};
// Initial data fetch
useEffect(() => {
fetchCategories();
}, []);
// Fetch products when filters change
useEffect(() => {
fetchProducts();
}, [selectedCategory, attributeFilters, groupByAttribute]);
// Destructure antd components
const { Title, Text } = Typography;
return (
<PageContainer title="品牌空间">
<div style={{ padding: '16px', background: '#fff' }}>
{/* Filter Section */}
<div style={{ marginBottom: '24px' }}>
<Title level={4} style={{ marginBottom: '16px' }}></Title>
<Space direction="vertical" size="large">
{/* Category Filter */}
<div>
<Text strong></Text>
<Select
placeholder="请选择分类"
style={{ width: 300, marginLeft: '8px' }}
value={selectedCategory}
onChange={setSelectedCategory}
allowClear
showSearch
optionFilterProp="children"
>
{categories.map(category => (
<Option key={category.id} value={category.name}>
{category.title}
</Option>
))}
</Select>
</div>
{/* Attribute Filters */}
{categoryAttributes.length > 0 && (
<div>
<Text strong></Text>
<Space direction="vertical" style={{ marginTop: '8px', width: '100%' }}>
{categoryAttributes.map(attr => (
<div key={attr.id} style={{ display: 'flex', alignItems: 'center' }}>
<Text style={{ width: '100px' }}>{attr.title}</Text>
<ProFromSelect
placeholder={`请选择${attr.title}`}
style={{ width: 300 }}
value={attributeFilters[attr.name] || null}
onChange={value => handleAttributeFilterChange(attr.name, value)}
allowClear
showSearch
optionFilterProp="children"
request={async (params) => {
try {
console.log('params', params,attr);
const response = await dictcontrollerGetdictitems({ dictId: attr.name });
const rawValues = Array.isArray(response) ? response : response?.data?.items || [];
const filteredValues = rawValues.filter((value: any) =>
value.dictId === attr.name || value.dict?.id === attr.name || value.dict?.name === attr.name
);
return {
options: filteredValues.map((value: any) => ({
label: `${value.name}${value.titleCN || value.title}`,
value: value.id
}))
};
} catch (error) {
console.error(`Failed to fetch ${attr.title} values:`, error);
message.error(`获取${attr.title}属性值失败`);
return { options: [] };
}
}}
/>
</div>
))}
</Space>
</div>
)}
{/* Group By Attribute */}
{categoryAttributes.length > 0 && (
<div>
<Text strong></Text>
<Select
placeholder="请选择分组属性"
style={{ width: 300, marginLeft: '8px' }}
value={groupByAttribute}
onChange={setGroupByAttribute}
showSearch
optionFilterProp="children"
>
{categoryAttributes.map(attr => (
<Option key={attr.id} value={attr.name}>
{attr.title}
</Option>
))}
</Select>
</div>
)}
</Space>
</div>
<Divider />
{/* Products Section */}
<div>
<Title level={4} style={{ marginBottom: '16px' }}> ({products.length} )</Title>
{loading ? (
<div style={{ textAlign: 'center', padding: '64px' }}>
<Text>...</Text>
</div>
) : groupByAttribute && Object.keys(groupedProducts).length > 0 ? (
<div style={{ display: 'flex', flexDirection: 'column', gap: '24px' }}>
{Object.entries(groupedProducts).map(([attrValueId, groupProducts]) => {
return (
<ProductGroup
key={attrValueId}
attributeValueId={attrValueId}
groupProducts={groupProducts}
// attributeValue={}
attributeName={groupByAttribute!}
/>
);
})}
</div>
) : (
<div style={{ textAlign: 'center', padding: '64px', background: '#fafafa', borderRadius: 8 }}>
<Text type="secondary"></Text>
</div>
)}
</div>
</div>
</PageContainer>
);
};
export default ProductGroupBy;