forked from yoone/WEB
parent
d2a84a9a4a
commit
2ee8964bce
|
|
@ -166,8 +166,8 @@ export default defineConfig({
|
|||
},
|
||||
{
|
||||
name: '产品品牌空间',
|
||||
path: '/product/brandspace',
|
||||
component: './Product/BrandSpace',
|
||||
path: '/product/groupBy',
|
||||
component: './Product/GroupBy',
|
||||
},
|
||||
// sync
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
Loading…
Reference in New Issue