487 lines
16 KiB
TypeScript
487 lines
16 KiB
TypeScript
import { PageContainer } from '@ant-design/pro-components';
|
|
import { request } from '@umijs/max';
|
|
import {
|
|
Card,
|
|
Col,
|
|
Image,
|
|
Layout,
|
|
Row,
|
|
Select,
|
|
Space,
|
|
Typography,
|
|
message,
|
|
} from 'antd';
|
|
import React, { useEffect, useState } from 'react';
|
|
|
|
const { Sider, Content } = Layout;
|
|
const { Title, Text } = Typography;
|
|
const { Option } = Select;
|
|
|
|
// Define interfaces
|
|
interface Brand {
|
|
id: number;
|
|
name: string;
|
|
shortName?: string;
|
|
image?: string;
|
|
}
|
|
|
|
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]: any };
|
|
}
|
|
|
|
const BrandSpace: React.FC = () => {
|
|
// State management
|
|
const [brands, setBrands] = useState<Brand[]>([]);
|
|
const [selectedBrand, setSelectedBrand] = useState<number | null>(null);
|
|
|
|
const [attributes, setAttributes] = useState<Attribute[]>([]);
|
|
const [selectedAttribute, setSelectedAttribute] = useState<string | null>(
|
|
null,
|
|
);
|
|
|
|
const [attributeValues, setAttributeValues] = useState<AttributeValue[]>([]);
|
|
const [selectedAttributeValue, setSelectedAttributeValue] = useState<
|
|
number | null
|
|
>(null);
|
|
|
|
const [products, setProducts] = useState<Product[]>([]);
|
|
const [loading, setLoading] = useState(false);
|
|
const [attributeValuesLoading, setAttributeValuesLoading] = useState(false);
|
|
|
|
// Fetch brands list
|
|
const fetchBrands = async () => {
|
|
try {
|
|
const response = await request('/dict/items', {
|
|
params: { dictId: 'brand' }, // Assuming brand is a dict
|
|
});
|
|
const brandList = Array.isArray(response)
|
|
? response
|
|
: response?.data || [];
|
|
setBrands(brandList);
|
|
|
|
// Set default brand to "yoone" if exists
|
|
const defaultBrand = brandList.find((brand) => brand.name === 'yoone');
|
|
if (defaultBrand) {
|
|
setSelectedBrand(defaultBrand.id);
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to fetch brands:', error);
|
|
message.error('获取品牌列表失败');
|
|
}
|
|
};
|
|
|
|
// Fetch attributes list
|
|
const fetchAttributes = async () => {
|
|
try {
|
|
// Get all dicts that are attributes (excluding non-attribute dicts)
|
|
const response = await request('/dict/list');
|
|
const dictList = Array.isArray(response)
|
|
? response
|
|
: response?.data || [];
|
|
|
|
// Filter out non-attribute dicts (assuming attributes are specific dicts)
|
|
const attributeDicts = dictList.filter((dict: any) =>
|
|
['strength', 'flavor', 'humidity', 'size', 'version'].includes(
|
|
dict.name,
|
|
),
|
|
);
|
|
|
|
setAttributes(attributeDicts);
|
|
|
|
// Set default attribute to strength if exists
|
|
const defaultAttribute = attributeDicts.find(
|
|
(attr) => attr.name === 'strength',
|
|
);
|
|
if (defaultAttribute) {
|
|
setSelectedAttribute(defaultAttribute.name);
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to fetch attributes:', error);
|
|
message.error('获取属性列表失败');
|
|
}
|
|
};
|
|
|
|
// Fetch attribute values based on selected attribute
|
|
const fetchAttributeValues = async (attributeName: string) => {
|
|
setAttributeValuesLoading(true);
|
|
try {
|
|
const response = await request('/dict/items', {
|
|
params: { dictId: attributeName },
|
|
});
|
|
const values = Array.isArray(response) ? response : response?.data || [];
|
|
setAttributeValues(values);
|
|
} catch (error) {
|
|
console.error('Failed to fetch attribute values:', error);
|
|
message.error('获取属性值列表失败');
|
|
// Clear attribute values on error
|
|
setAttributeValues([]);
|
|
} finally {
|
|
setAttributeValuesLoading(false);
|
|
}
|
|
};
|
|
|
|
// Fetch products based on filters
|
|
const fetchProducts = async () => {
|
|
if (!selectedBrand) return;
|
|
|
|
setLoading(true);
|
|
try {
|
|
const params: any = {
|
|
brandId: selectedBrand,
|
|
};
|
|
|
|
// Add attribute filter if selected
|
|
if (selectedAttribute) {
|
|
// If attribute value is selected, filter by both attribute and value
|
|
if (selectedAttributeValue) {
|
|
params[selectedAttribute] = selectedAttributeValue;
|
|
} else {
|
|
// If only attribute is selected, filter by attribute presence
|
|
params[selectedAttribute] = 'hasValue';
|
|
}
|
|
}
|
|
|
|
const response = await request('/product/list', {
|
|
params,
|
|
});
|
|
|
|
const productList = response?.data?.items || [];
|
|
setProducts(productList);
|
|
} catch (error) {
|
|
console.error('Failed to fetch products:', error);
|
|
message.error('获取产品列表失败');
|
|
// Clear products on error
|
|
setProducts([]);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
// Initial data fetch
|
|
useEffect(() => {
|
|
fetchBrands();
|
|
fetchAttributes();
|
|
}, []);
|
|
|
|
// Fetch attribute values when attribute changes
|
|
useEffect(() => {
|
|
if (selectedAttribute) {
|
|
fetchAttributeValues(selectedAttribute);
|
|
setSelectedAttributeValue(null); // Reset selected value when attribute changes
|
|
}
|
|
}, [selectedAttribute]);
|
|
|
|
// Fetch products when filters change
|
|
useEffect(() => {
|
|
fetchProducts();
|
|
}, [selectedBrand, selectedAttribute, selectedAttributeValue]);
|
|
|
|
// Handle brand selection change
|
|
const handleBrandChange = (value: number) => {
|
|
setSelectedBrand(value);
|
|
};
|
|
|
|
// Handle attribute selection change
|
|
const handleAttributeChange = (value: string) => {
|
|
setSelectedAttribute(value);
|
|
};
|
|
|
|
// Handle attribute value selection change
|
|
const handleAttributeValueChange = (value: number) => {
|
|
setSelectedAttributeValue(value);
|
|
};
|
|
|
|
return (
|
|
<PageContainer title="品牌空间">
|
|
<Layout style={{ minHeight: 'calc(100vh - 64px)', background: '#fff' }}>
|
|
{/* Top Brand Selection */}
|
|
<div style={{ padding: '16px', borderBottom: '1px solid #f0f0f0' }}>
|
|
<Space direction="vertical" style={{ width: '100%' }}>
|
|
<Title level={4} style={{ margin: 0 }}>
|
|
选择品牌
|
|
</Title>
|
|
<Select
|
|
placeholder="请选择品牌"
|
|
style={{ width: 300 }}
|
|
value={selectedBrand}
|
|
onChange={handleBrandChange}
|
|
allowClear
|
|
>
|
|
{brands.map((brand) => (
|
|
<Option key={brand.id} value={brand.id}>
|
|
{brand.name}
|
|
</Option>
|
|
))}
|
|
</Select>
|
|
</Space>
|
|
</div>
|
|
|
|
<Layout>
|
|
{/* Left Attribute Selection */}
|
|
<Sider
|
|
width={240}
|
|
style={{ background: '#fafafa', borderRight: '1px solid #f0f0f0' }}
|
|
>
|
|
<div style={{ padding: '16px' }}>
|
|
<Space direction="vertical" style={{ width: '100%' }}>
|
|
<Title level={5} style={{ margin: 0 }}>
|
|
选择属性
|
|
</Title>
|
|
<Select
|
|
placeholder="请选择属性类型"
|
|
style={{ width: '100%' }}
|
|
value={selectedAttribute}
|
|
onChange={handleAttributeChange}
|
|
allowClear
|
|
>
|
|
{attributes.map((attr) => (
|
|
<Option key={attr.id} value={attr.name}>
|
|
{attr.title}
|
|
</Option>
|
|
))}
|
|
</Select>
|
|
|
|
{selectedAttribute && (
|
|
<>
|
|
<Title level={5} style={{ margin: '16px 0 8px 0' }}>
|
|
属性值
|
|
</Title>
|
|
<Select
|
|
placeholder={`请选择${
|
|
attributes.find((a) => a.name === selectedAttribute)
|
|
?.title
|
|
}`}
|
|
style={{ width: '100%' }}
|
|
value={selectedAttributeValue}
|
|
onChange={handleAttributeValueChange}
|
|
allowClear
|
|
loading={attributeValuesLoading}
|
|
>
|
|
{attributeValues.map((value) => (
|
|
<Option key={value.id} value={value.id}>
|
|
<Space>
|
|
{value.image && (
|
|
<Image
|
|
src={value.image}
|
|
style={{
|
|
width: 24,
|
|
height: 24,
|
|
objectFit: 'cover',
|
|
borderRadius: 4,
|
|
}}
|
|
/>
|
|
)}
|
|
<span>
|
|
{value.titleCN || value.title || value.name}
|
|
</span>
|
|
</Space>
|
|
</Option>
|
|
))}
|
|
</Select>
|
|
</>
|
|
)}
|
|
|
|
{/* Filter Summary */}
|
|
{selectedBrand && (
|
|
<div
|
|
style={{
|
|
marginTop: 24,
|
|
padding: 12,
|
|
background: '#fff',
|
|
borderRadius: 8,
|
|
}}
|
|
>
|
|
<Text strong>当前筛选条件:</Text>
|
|
<div style={{ marginTop: 8 }}>
|
|
<Text type="secondary">品牌: </Text>
|
|
<Text>
|
|
{brands.find((b) => b.id === selectedBrand)?.name}
|
|
</Text>
|
|
</div>
|
|
{selectedAttribute && (
|
|
<div style={{ marginTop: 4 }}>
|
|
<Text type="secondary">
|
|
{
|
|
attributes.find((a) => a.name === selectedAttribute)
|
|
?.title
|
|
}
|
|
:
|
|
</Text>
|
|
<Text>
|
|
{selectedAttributeValue
|
|
? attributeValues.find(
|
|
(v) => v.id === selectedAttributeValue,
|
|
)?.titleCN ||
|
|
attributeValues.find(
|
|
(v) => v.id === selectedAttributeValue,
|
|
)?.title
|
|
: '所有值'}
|
|
</Text>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</Space>
|
|
</div>
|
|
</Sider>
|
|
|
|
{/* Main Content - Product List */}
|
|
<Content style={{ padding: '16px', overflowX: 'auto' }}>
|
|
<div style={{ marginBottom: 16 }}>
|
|
<Title level={4} style={{ margin: 0 }}>
|
|
产品列表
|
|
<Text type="secondary" style={{ fontSize: 16, marginLeft: 8 }}>
|
|
({products.length} 个产品)
|
|
</Text>
|
|
</Title>
|
|
</div>
|
|
|
|
{loading ? (
|
|
<div style={{ textAlign: 'center', padding: '64px' }}>
|
|
<Text>加载中...</Text>
|
|
</div>
|
|
) : attributeValues.length > 0 ? (
|
|
<div style={{ minWidth: 'max-content' }}>
|
|
<Row gutter={[16, 16]}>
|
|
{attributeValues.map((attributeValue) => {
|
|
// Filter products for this attribute value
|
|
if(!Array.isArray(products)){
|
|
console.log('products',products)
|
|
return null
|
|
}
|
|
const attributeValueProducts = selectedAttribute
|
|
? (products || []).filter((product) =>
|
|
product.attributes &&
|
|
product.attributes[selectedAttribute] === attributeValue.id
|
|
)
|
|
: [];
|
|
|
|
return (
|
|
<Col key={attributeValue.id} style={{ minWidth: 300 }}>
|
|
<Card
|
|
title={
|
|
<Space>
|
|
{attributeValue.image && (
|
|
<Image
|
|
src={attributeValue.image}
|
|
style={{
|
|
width: 24,
|
|
height: 24,
|
|
objectFit: 'cover',
|
|
borderRadius: 4,
|
|
}}
|
|
/>
|
|
)}
|
|
<span>
|
|
{attributeValue.titleCN || attributeValue.title || attributeValue.name}
|
|
</span>
|
|
</Space>
|
|
}
|
|
style={{ width: 300 }}
|
|
>
|
|
{attributeValueProducts.length > 0 ? (
|
|
<div style={{ maxHeight: 600, overflowY: 'auto' }}>
|
|
{attributeValueProducts.map((product) => (
|
|
<Card
|
|
key={product.id}
|
|
size="small"
|
|
hoverable
|
|
style={{ marginBottom: 8 }}
|
|
>
|
|
<div
|
|
style={{
|
|
height: 100,
|
|
overflow: 'hidden',
|
|
marginBottom: 8,
|
|
}}
|
|
>
|
|
<Image
|
|
src={
|
|
product.image ||
|
|
'https://via.placeholder.com/100x100?text=No+Image'
|
|
}
|
|
alt={product.name}
|
|
style={{
|
|
width: '100%',
|
|
height: '100%',
|
|
objectFit: 'cover',
|
|
}}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<Text
|
|
type="secondary"
|
|
style={{ fontSize: 12, marginBottom: 4 }}
|
|
>
|
|
{product.sku}
|
|
</Text>
|
|
<div style={{ marginTop: 4 }}>
|
|
<Text ellipsis style={{ width: '100%', display: 'block' }}>
|
|
{product.name}
|
|
</Text>
|
|
</div>
|
|
<div style={{ marginTop: 8 }}>
|
|
<Text
|
|
strong
|
|
style={{ fontSize: 14, color: '#ff4d4f' }}
|
|
>
|
|
¥{product.price || '--'}
|
|
</Text>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<div style={{ textAlign: 'center', padding: '32px' }}>
|
|
<Text type="secondary">暂无产品</Text>
|
|
</div>
|
|
)}
|
|
</Card>
|
|
</Col>
|
|
);
|
|
})}
|
|
</Row>
|
|
</div>
|
|
) : (
|
|
<div
|
|
style={{
|
|
textAlign: 'center',
|
|
padding: '64px',
|
|
background: '#fafafa',
|
|
borderRadius: 8,
|
|
}}
|
|
>
|
|
<Text type="secondary">暂无属性值或产品</Text>
|
|
</div>
|
|
)}
|
|
</Content>
|
|
</Layout>
|
|
</Layout>
|
|
</PageContainer>
|
|
);
|
|
};
|
|
|
|
export default BrandSpace;
|