369 lines
13 KiB
TypeScript
369 lines
13 KiB
TypeScript
import React, { useEffect, useState, useMemo } from 'react';
|
||
import { PageContainer, ProFormSelect } 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>
|
||
<ProFormSelect
|
||
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;
|