diff --git a/.umirc.ts b/.umirc.ts index 0063d2b..4889d7d 100644 --- a/.umirc.ts +++ b/.umirc.ts @@ -134,9 +134,14 @@ export default defineConfig({ }, { name: '数据分析列表', - path: '/customer/statistic', - component: './Customer/Statistic', + path: '/customer/statistic/list', + component: './Customer/StatisticList', }, + // { + // name: '客户统计', + // path: '/customer/statistic/home', + // component: './Customer/Statistic', + // } ], }, { @@ -166,8 +171,8 @@ export default defineConfig({ }, { name: '产品品牌空间', - path: '/product/brandspace', - component: './Product/BrandSpace', + path: '/product/groupBy', + component: './Product/GroupBy', }, // sync { diff --git a/src/components/SyncForm.tsx b/src/components/SyncForm.tsx index 334e90e..9d5d260 100644 --- a/src/components/SyncForm.tsx +++ b/src/components/SyncForm.tsx @@ -4,8 +4,8 @@ import { ActionType, DrawerForm, ProForm, - ProFormSelect, ProFormDateRangePicker, + ProFormSelect, } from '@ant-design/pro-components'; import { Button } from 'antd'; import dayjs from 'dayjs'; @@ -24,7 +24,12 @@ interface SyncFormProps { * @param {SyncFormProps} props 组件属性 * @returns {React.ReactElement} 抽屉表单 */ -const SyncForm: React.FC = ({ tableRef, onFinish, siteId, dateRange }) => { +const SyncForm: React.FC = ({ + tableRef, + onFinish, + siteId, + dateRange, +}) => { // 使用 antd 的 App 组件提供的 message API const [loading, setLoading] = React.useState(false); @@ -52,11 +57,10 @@ const SyncForm: React.FC = ({ tableRef, onFinish, siteId, dateRan // 返回一个抽屉表单 return ( - initialValues={{ - dateRange: [dayjs().subtract(1, 'week'), dayjs()], - }} - - title="同步订单" + initialValues={{ + dateRange: [dayjs().subtract(1, 'week'), dayjs()], + }} + title="同步订单" // 表单的触发器,一个带图标的按钮 trigger={ } - width={800} - modalProps={{ - destroyOnHidden: true, - }} - submitter={false} - > - { - const { data, success } = await customercontrollerGettags(); - if (!success) return []; - setTagList(tags || []); - return data - .filter((tag) => { - return !(tags || []).includes(tag); - }) - .map((tag) => ({ label: tag, value: tag })); - }} - fieldProps={{ - value: tagList, // 当前值 - onChange: async (newValue) => { - const added = newValue.filter((x) => !tagList.includes(x)); - const removed = tagList.filter((x) => !newValue.includes(x)); - - if (added.length) { - const { success, message: msg } = await customercontrollerAddtag({ - email, - tag: added[0], - }); - if (!success) { - message.error(msg); - return; - } - } - if (removed.length) { - const { success, message: msg } = await customercontrollerDeltag({ - email, - tag: removed[0], - }); - if (!success) { - message.error(msg); - return; - } - } - tableRef?.current?.reload(); - - setTagList(newValue); - }, - }} - > - - ); -}; - -export default ListPage; +} \ No newline at end of file diff --git a/src/pages/Customer/StatisticList/index.tsx b/src/pages/Customer/StatisticList/index.tsx new file mode 100644 index 0000000..09d098d --- /dev/null +++ b/src/pages/Customer/StatisticList/index.tsx @@ -0,0 +1,289 @@ +import { HistoryOrder } from '@/pages/Statistics/Order'; +import { + customercontrollerAddtag, + customercontrollerDeltag, + customercontrollerGetcustomerlist, + customercontrollerGettags, + customercontrollerSetrate, +} from '@/servers/api/customer'; +import { + ActionType, + ModalForm, + PageContainer, + ProColumns, + ProFormSelect, + ProTable, +} from '@ant-design/pro-components'; +import { App, Button, Rate, Space, Tag } from 'antd'; +import dayjs from 'dayjs'; +import { useRef, useState } from 'react'; + +const ListPage: React.FC = () => { + const actionRef = useRef(); + const { message } = App.useApp(); + const columns: ProColumns[] = [ + { + title: '用户名', + dataIndex: 'username', + hideInSearch: true, + render: (_, record) => { + if (record.billing.first_name || record.billing.last_name) + return record.billing.first_name + ' ' + record.billing.last_name; + return record.shipping.first_name + ' ' + record.shipping.last_name; + }, + }, + { + title: '邮箱', + dataIndex: 'email', + }, + { + title: '客户编号', + dataIndex: 'customerId', + render: (_, record) => { + if (!record.customerId) return '-'; + return String(record.customerId).padStart(6, 0); + }, + sorter: true, + }, + { + title: '首单时间', + dataIndex: 'first_purchase_date', + valueType: 'dateMonth', + sorter: true, + render: (_, record) => + record.first_purchase_date + ? dayjs(record.first_purchase_date).format('YYYY-MM-DD HH:mm:ss') + : '-', + // search: { + // transform: (value: string) => { + // return { month: value }; + // }, + // }, + }, + { + title: '尾单时间', + hideInSearch: true, + dataIndex: 'last_purchase_date', + valueType: 'dateTime', + sorter: true, + }, + { + title: '订单数', + dataIndex: 'orders', + hideInSearch: true, + sorter: true, + }, + { + title: '金额', + dataIndex: 'total', + hideInSearch: true, + sorter: true, + }, + { + title: 'YOONE订单数', + dataIndex: 'yoone_orders', + hideInSearch: true, + sorter: true, + }, + { + title: 'YOONE金额', + dataIndex: 'yoone_total', + hideInSearch: true, + sorter: true, + }, + { + title: '等级', + hideInSearch: true, + render: (_, record) => { + if (!record.yoone_orders || !record.yoone_total) return '-'; + if (Number(record.yoone_orders) === 1 && Number(record.yoone_total) > 0) + return 'B'; + return '-'; + }, + }, + { + title: '评星', + dataIndex: 'rate', + width: 200, + render: (_, record) => { + return ( + { + try { + const { success, message: msg } = + await customercontrollerSetrate({ + id: record.customerId, + rate: val, + }); + if (success) { + message.success(msg); + actionRef.current?.reload(); + } + } catch (e) { + message.error(e.message); + } + }} + value={record.rate} + /> + ); + }, + }, + { + title: '联系电话', + dataIndex: 'phone', + hideInSearch: true, + render: (_, record) => record?.billing.phone || record?.shipping.phone, + }, + { + title: '账单地址', + dataIndex: 'billing', + render: (_, record) => + JSON.stringify(record?.billing || record?.shipping), + }, + { + title: '标签', + dataIndex: 'tags', + render: (_, record) => { + return ( + + {(record.tags || []).map((tag) => { + return ( + { + const { success, message: msg } = + await customercontrollerDeltag({ + email: record.email, + tag, + }); + return false; + }} + > + {tag} + + ); + })} + + ); + }, + }, + { + title: '操作', + dataIndex: 'option', + valueType: 'option', + fixed: 'right', + render: (_, record) => { + return ( + + + + + ); + }, + }, + ]; + return ( + + { + const key = Object.keys(sorter)[0]; + const { data, success } = await customercontrollerGetcustomerlist({ + ...params, + ...(key ? { sorterKey: key, sorterValue: sorter[key] } : {}), + }); + + return { + total: data?.total || 0, + data: data?.items || [], + success, + }; + }} + columns={columns} + /> + + ); +}; + +export const AddTag: React.FC<{ + email: string; + tags?: string[]; + tableRef: React.MutableRefObject; +}> = ({ email, tags, tableRef }) => { + const { message } = App.useApp(); + const [tagList, setTagList] = useState([]); + + return ( + 修改标签} + width={800} + modalProps={{ + destroyOnHidden: true, + }} + submitter={false} + > + { + const { data, success } = await customercontrollerGettags(); + if (!success) return []; + setTagList(tags || []); + return data + .filter((tag) => { + return !(tags || []).includes(tag); + }) + .map((tag) => ({ label: tag, value: tag })); + }} + fieldProps={{ + value: tagList, // 当前值 + onChange: async (newValue) => { + const added = newValue.filter((x) => !tagList.includes(x)); + const removed = tagList.filter((x) => !newValue.includes(x)); + + if (added.length) { + const { success, message: msg } = await customercontrollerAddtag({ + email, + tag: added[0], + }); + if (!success) { + message.error(msg); + return; + } + } + if (removed.length) { + const { success, message: msg } = await customercontrollerDeltag({ + email, + tag: removed[0], + }); + if (!success) { + message.error(msg); + return; + } + } + tableRef?.current?.reload(); + + setTagList(newValue); + }, + }} + > + + ); +}; + +export default ListPage; diff --git a/src/pages/Order/List/index.tsx b/src/pages/Order/List/index.tsx index 20a534e..082aa39 100644 --- a/src/pages/Order/List/index.tsx +++ b/src/pages/Order/List/index.tsx @@ -502,7 +502,10 @@ const ListPage: React.FC = () => { success, message: errMsg, data, - } = await ordercontrollerSyncorders(values,{after:values.dateRange?.[0]+'T00:00:00Z',before:values.dateRange?.[1]+'T23:59:59Z'}); + } = await ordercontrollerSyncorders(values, { + after: values.dateRange?.[0] + 'T00:00:00Z', + before: values.dateRange?.[1] + 'T23:59:59Z', + }); if (!success) { throw new Error(errMsg); } @@ -990,7 +993,7 @@ const Detail: React.FC<{
    {record?.items?.map((item: any) => (
  • - {item.name}:{item.quantity} + {item.name}({item.sku}):{item.quantity}
  • ))}
@@ -1015,7 +1018,7 @@ const Detail: React.FC<{
    {record?.sales?.map((item: any) => (
  • - {item.name}:{item.quantity} + {item.name}({item.sku}):{item.quantity}
  • ))}
@@ -1507,16 +1510,16 @@ const Shipping: React.FC<{ - // value && value.length > 0 - // ? Promise.resolve() - // : Promise.reject('至少需要一个商品'), - // }, - // ]} + // rules={[ + // { + // required: true, + // message: '至少需要一个商品', + // validator: (_, value) => + // value && value.length > 0 + // ? Promise.resolve() + // : Promise.reject('至少需要一个商品'), + // }, + // ]} > { - // State management - const [brands, setBrands] = useState([]); - const [selectedBrand, setSelectedBrand] = useState(null); - - const [attributes, setAttributes] = useState([]); - const [selectedAttribute, setSelectedAttribute] = useState( - null, - ); - - const [attributeValues, setAttributeValues] = useState([]); - const [selectedAttributeValue, setSelectedAttributeValue] = useState< - number | null - >(null); - - const [products, setProducts] = useState([]); - 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 ( - - - {/* Top Brand Selection */} -
- - - 选择品牌 - - - -
- - - {/* Left Attribute Selection */} - -
- - - 选择属性 - - - - {selectedAttribute && ( - <> - - 属性值 - - - - )} - - {/* Filter Summary */} - {selectedBrand && ( -
- 当前筛选条件: -
- 品牌: - - {brands.find((b) => b.id === selectedBrand)?.name} - -
- {selectedAttribute && ( -
- - { - attributes.find((a) => a.name === selectedAttribute) - ?.title - } - : - - - {selectedAttributeValue - ? attributeValues.find( - (v) => v.id === selectedAttributeValue, - )?.titleCN || - attributeValues.find( - (v) => v.id === selectedAttributeValue, - )?.title - : '所有值'} - -
- )} -
- )} -
-
-
- - {/* Main Content - Product List */} - -
- - 产品列表 - <Text type="secondary" style={{ fontSize: 16, marginLeft: 8 }}> - ({products.length} 个产品) - </Text> - -
- - {loading ? ( -
- 加载中... -
- ) : attributeValues.length > 0 ? ( -
- - {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 ( - - - {attributeValue.image && ( - - )} - - {attributeValue.titleCN || attributeValue.title || attributeValue.name} - - - } - style={{ width: 300 }} - > - {attributeValueProducts.length > 0 ? ( -
- {attributeValueProducts.map((product) => ( - -
- {product.name} -
-
- - {product.sku} - -
- - {product.name} - -
-
- - ¥{product.price || '--'} - -
-
-
- ))} -
- ) : ( -
- 暂无产品 -
- )} -
- - ); - })} -
-
- ) : ( -
- 暂无属性值或产品 -
- )} -
-
-
-
- ); -}; - -export default BrandSpace; diff --git a/src/pages/Product/CsvTool/index.tsx b/src/pages/Product/CsvTool/index.tsx index 503ad11..02bded3 100644 --- a/src/pages/Product/CsvTool/index.tsx +++ b/src/pages/Product/CsvTool/index.tsx @@ -415,11 +415,11 @@ const CsvTool: React.FC = () => { } // 如果type为single且启用了生成bundle SKU,则添加quantity - if ( - quantity - ) { + if (quantity) { // 使用quantity的shortName,如果没有则使用quantity但匹配 4 个零 - const quantityShortName = attributeMappings.quantities[quantity] || Number(quantity).toString().padStart(4, '0'); + const quantityShortName = + attributeMappings.quantities[quantity] || + Number(quantity).toString().padStart(4, '0'); skuComponents.push(quantityShortName); } // 合并所有组件,使用短横线分隔 @@ -463,9 +463,7 @@ const CsvTool: React.FC = () => { if (size) nameComponents.push(size); // 如果有数量且类型为bundle或者生成bundle的single产品,则添加数量 - if ( - type==='bundle' && quantity - ) { + if (type === 'bundle' && quantity) { nameComponents.push(String(quantity)); } @@ -569,7 +567,7 @@ const CsvTool: React.FC = () => { // Determine which data to use for processing and download let finalData = dataWithSku; - console.log('generateBundleSkuForSingle',generateBundleSkuForSingle) + console.log('generateBundleSkuForSingle', generateBundleSkuForSingle); // If generateBundleSkuForSingle is enabled, generate bundle products for single products if (generateBundleSkuForSingle) { // Filter out single records @@ -578,7 +576,9 @@ const CsvTool: React.FC = () => { ); // Get quantity values from the config (same source as other attributes like brand) - const quantityValues = config.quantities.map(quantity=>quantity.name) + const quantityValues = config.quantities.map( + (quantity) => quantity.name, + ); // Generate bundle products for each single record and quantity const generatedBundleRecords = singleRecords.flatMap((singleRecord) => { @@ -772,7 +772,11 @@ const CsvTool: React.FC = () => { valuePropName="checked" initialValue={true} > - setGenerateBundleSkuForSingle(e.target.checked)}> + + setGenerateBundleSkuForSingle(e.target.checked) + } + > 启用为single类型生成bundle SKU diff --git a/src/pages/Product/GroupBy/index.tsx b/src/pages/Product/GroupBy/index.tsx new file mode 100644 index 0000000..44a34c4 --- /dev/null +++ b/src/pages/Product/GroupBy/index.tsx @@ -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 ( + + {/*
+ {product.name} +
*/} +
+ + {product.sku} + + + {product.name} + + + ¥{product.price || '--'} + +
+
+ ); +}; + +// 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 = ( + + {attributeValue?.image && ( + + )} + + + {attributeValue?.titleCN || attributeValue?.title || attributeValue?.name || attributeValueId||'未知'} + (共 {groupProducts.length} 个产品) + + + + ); + + return ( + setIsCollapsed(Array.isArray(key) && key.length === 0)} + ghost + bordered={false} + items={[ + { + key: attributeValueId, + label: panelHeader, + children: ( +
+ {groupProducts.map((product) => ( + + ))} +
+ ), + }, + ]} + /> + ); +}; + +// Main ProductGroupBy component +const ProductGroupBy: React.FC = () => { + // State management + const [categories, setCategories] = useState([]); + const [selectedCategory, setSelectedCategory] = useState(null); + // Store selected values for each attribute + const [attributeFilters, setAttributeFilters] = useState<{ [key: string]: number | null }>({}); + + // Group by attribute + const [groupByAttribute, setGroupByAttribute] = useState(null); + + // Products + const [products, setProducts] = useState([]); + const [groupedProducts, setGroupedProducts] = useState({}); + 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 ( + +
+ {/* Filter Section */} +
+ 筛选条件 + + {/* Category Filter */} +
+ 选择分类: + +
+ + {/* Attribute Filters */} + {categoryAttributes.length > 0 && ( +
+ 属性筛选: + + {categoryAttributes.map(attr => ( +
+ {attr.title}: + 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: [] }; + } + }} + /> +
+ ))} +
+
+ )} + + {/* Group By Attribute */} + {categoryAttributes.length > 0 && ( +
+ 分组依据: + +
+ )} +
+
+ + + + {/* Products Section */} +
+ 产品列表 ({products.length} 个产品) + + {loading ? ( +
+ 加载中... +
+ ) : groupByAttribute && Object.keys(groupedProducts).length > 0 ? ( +
+ {Object.entries(groupedProducts).map(([attrValueId, groupProducts]) => { + return ( + + ); + })} +
+ ) : ( +
+ 暂无产品 +
+ )} +
+
+
+ ); +}; + +export default ProductGroupBy; diff --git a/src/pages/Product/List/index.tsx b/src/pages/Product/List/index.tsx index 3615ce6..3f8b41c 100644 --- a/src/pages/Product/List/index.tsx +++ b/src/pages/Product/List/index.tsx @@ -235,10 +235,10 @@ const List: React.FC = () => { ), }, { - title: "图片", + title: '图片', dataIndex: 'image', width: 100, - valueType:'image' + valueType: 'image', }, { title: '名称', @@ -254,7 +254,6 @@ const List: React.FC = () => { ); }, }, - { title: '价格', diff --git a/src/pages/Site/List/index.tsx b/src/pages/Site/List/index.tsx index 4f02795..1b0e767 100644 --- a/src/pages/Site/List/index.tsx +++ b/src/pages/Site/List/index.tsx @@ -58,7 +58,7 @@ export interface SiteItem { const SiteList: React.FC = () => { const actionRef = useRef(); const [open, setOpen] = useState(false); - const [editing, setEditing] = useState(null); + const [editing, setEditing] = useState(null); const [selectedRowKeys, setSelectedRowKeys] = useState([]); const [batchEditOpen, setBatchEditOpen] = useState(false); const [batchEditForm] = Form.useForm(); @@ -202,8 +202,12 @@ const SiteList: React.FC = () => { ), }, - { title: 'webhook地址', dataIndex: 'webhookUrl', width: 280, hideInSearch: true }, - + { + title: 'webhook地址', + dataIndex: 'webhookUrl', + hideInSearch: true, + }, + { title: 'SKU 前缀', dataIndex: 'skuPrefix', @@ -288,7 +292,13 @@ const SiteList: React.FC = () => {