zksu
/
WEB
forked from yoone/WEB
1
0
Fork 0
WEB/src/pages/Product/GroupBy/index.tsx

369 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;