From d2a84a9a4ae7e1ff5c40e63ed26edd7526c3e503 Mon Sep 17 00:00:00 2001 From: tikkhun Date: Sat, 17 Jan 2026 14:28:21 +0800 Subject: [PATCH 1/6] =?UTF-8?q?feat(api)=20=E8=BF=90=E8=A1=8C=20openapit2t?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/Order/List/index.tsx | 4 +- src/servers/api/index.ts | 4 +- src/servers/api/product.ts | 30 ++++++++++ src/servers/api/statistics.ts | 2 +- src/servers/api/typings.d.ts | 104 +++++++++++++++++++++++++++++---- 5 files changed, 128 insertions(+), 16 deletions(-) diff --git a/src/pages/Order/List/index.tsx b/src/pages/Order/List/index.tsx index 20a534e..963162b 100644 --- a/src/pages/Order/List/index.tsx +++ b/src/pages/Order/List/index.tsx @@ -990,7 +990,7 @@ const Detail: React.FC<{ @@ -1015,7 +1015,7 @@ const Detail: React.FC<{ diff --git a/src/servers/api/index.ts b/src/servers/api/index.ts index 2a640a5..02f2319 100644 --- a/src/servers/api/index.ts +++ b/src/servers/api/index.ts @@ -1,7 +1,7 @@ // @ts-ignore /* eslint-disable */ -// API 更新时间: -// API 唯一标识: +// API 更新时间: +// API 唯一标识: import * as area from './area'; import * as category from './category'; import * as customer from './customer'; diff --git a/src/servers/api/product.ts b/src/servers/api/product.ts index d23b585..45deca0 100644 --- a/src/servers/api/product.ts +++ b/src/servers/api/product.ts @@ -125,6 +125,21 @@ export async function productcontrollerBindproductsiteskus( }); } +/** 此处后端没有提供注释 GET /product/all */ +export async function productcontrollerGetallproducts( + // 叠加生成的Param类型 (非body参数swagger默认没有生成对象) + params: API.productcontrollerGetallproductsParams, + options?: { [key: string]: any }, +) { + return request('/product/all', { + method: 'GET', + params: { + ...params, + }, + ...(options || {}), + }); +} + /** 此处后端没有提供注释 GET /product/attribute */ export async function productcontrollerGetattributelist( // 叠加生成的Param类型 (非body参数swagger默认没有生成对象) @@ -539,6 +554,21 @@ export async function productcontrollerCompatflavorsall(options?: { }); } +/** 此处后端没有提供注释 GET /product/grouped */ +export async function productcontrollerGetgroupedproducts( + // 叠加生成的Param类型 (非body参数swagger默认没有生成对象) + params: API.productcontrollerGetgroupedproductsParams, + options?: { [key: string]: any }, +) { + return request('/product/grouped', { + method: 'GET', + params: { + ...params, + }, + ...(options || {}), + }); +} + /** 此处后端没有提供注释 POST /product/import */ export async function productcontrollerImportproductscsv( body: {}, diff --git a/src/servers/api/statistics.ts b/src/servers/api/statistics.ts index 5e3990a..dfc9be2 100644 --- a/src/servers/api/statistics.ts +++ b/src/servers/api/statistics.ts @@ -78,7 +78,7 @@ export async function statisticscontrollerGetorderbyemail( } /** 此处后端没有提供注释 GET /statistics/orderSource */ -export async function statisticscontrollerGetordersorce(options?: { +export async function statisticscontrollerGetordersource(options?: { [key: string]: any; }) { return request('/statistics/orderSource', { diff --git a/src/servers/api/typings.d.ts b/src/servers/api/typings.d.ts index a365f72..ab7f001 100644 --- a/src/servers/api/typings.d.ts +++ b/src/servers/api/typings.d.ts @@ -145,6 +145,8 @@ declare namespace API { price?: number; /** 促销价格 */ promotionPrice?: number; + /** 产品图片URL */ + image?: string; /** 属性列表 */ attributes?: any[]; /** 商品类型 */ @@ -266,6 +268,8 @@ declare namespace API { sku?: string; /** 分类ID (DictItem ID) */ categoryId?: number; + /** 分类名称 */ + categoryName?: string; /** 站点 SKU 列表 */ siteSkus?: any[]; /** 属性列表 */ @@ -274,6 +278,8 @@ declare namespace API { price?: number; /** 促销价格 */ promotionPrice?: number; + /** 产品图片URL */ + image?: string; /** 商品类型 */ type?: 'single' | 'bundle'; /** 产品组成 */ @@ -645,6 +651,10 @@ declare namespace API { createdAt: string; /** 更新时间 */ updatedAt: string; + /** 订单项列表 */ + orderItems?: any; + /** 销售项列表 */ + orderSales?: any; }; type OrderAddress = { @@ -695,28 +705,48 @@ declare namespace API { }; type ordercontrollerGetorderitemsParams = { + /** 是否为原产品还是库存产品 */ isSource?: boolean; - exceptPackage?: boolean; /** 页码 */ current?: number; /** 每页大小 */ pageSize?: number; + /** 排序对象,格式如 { productName: "asc", sku: "desc" } */ + orderBy?: any; + /** 是否排除套餐 */ + exceptPackage?: boolean; + /** 站点ID */ siteId?: number; + /** 名称 */ name?: string; + /** SKU */ + sku?: string; + /** 开始日期 */ startDate?: string; + /** 结束日期 */ endDate?: string; }; type ordercontrollerGetordersalesParams = { + /** 是否为原产品还是库存产品 */ isSource?: boolean; - exceptPackage?: boolean; /** 页码 */ current?: number; /** 每页大小 */ pageSize?: number; + /** 排序对象,格式如 { productName: "asc", sku: "desc" } */ + orderBy?: any; + /** 是否排除套餐 */ + exceptPackage?: boolean; + /** 站点ID */ siteId?: number; + /** 名称 */ name?: string; + /** SKU */ + sku?: string; + /** 开始日期 */ startDate?: string; + /** 结束日期 */ endDate?: string; }; @@ -820,6 +850,10 @@ declare namespace API { createdAt: string; /** 更新时间 */ updatedAt: string; + /** 订单项列表 */ + orderItems?: any; + /** 销售项列表 */ + orderSales?: any; items?: OrderItem[]; sales?: OrderSale[]; refundItems?: OrderRefundItem[]; @@ -935,10 +969,19 @@ declare namespace API { sku?: string; quantity?: number; isPackage?: boolean; - isYoone?: boolean; - isZex?: boolean; - size?: number; - isYooneNew?: boolean; + /** 商品品类 */ + category?: string; + /** 品牌 */ + brand?: string; + /** 口味 */ + flavor?: string; + /** 湿度 */ + humidity?: string; + /** 尺寸 */ + size?: string; + strength?: string; + /** 版本 */ + version?: string; /** 创建时间 */ createdAt?: string; /** 更新时间 */ @@ -956,10 +999,19 @@ declare namespace API { sku?: string; quantity?: number; isPackage?: boolean; - isYoone?: boolean; - isZex?: boolean; - size?: number; - isYooneNew?: boolean; + /** 商品品类 */ + category?: string; + /** 品牌 */ + brand?: string; + /** 口味 */ + flavor?: string; + /** 湿度 */ + humidity?: string; + /** 尺寸 */ + size?: string; + strength?: string; + /** 版本 */ + version?: string; /** 创建时间 */ createdAt?: string; /** 更新时间 */ @@ -994,6 +1046,7 @@ declare namespace API { endDate?: string; keyword?: string; siteId?: number; + country?: any; purchaseType?: 'all' | 'first_purchase' | 'repeat_purchase'; orderType?: 'all' | 'cpc' | 'non_cpc'; brand?: 'all' | 'zyn' | 'yoone' | 'zolt'; @@ -1034,10 +1087,14 @@ declare namespace API { shortDescription?: string; /** 产品描述 */ description?: string; + /** 产品图片URL */ + image?: string; /** 价格 */ price?: number; /** 促销价格 */ promotionPrice?: number; + /** 分类 ID */ + categoryId?: number; /** 库存组成 */ components?: ProductStockComponent[]; /** 站点 SKU 列表 */ @@ -1134,6 +1191,10 @@ declare namespace API { id: number; }; + type productcontrollerGetallproductsParams = { + brand?: string; + }; + type productcontrollerGetattributeallParams = { dictName?: string; }; @@ -1149,6 +1210,11 @@ declare namespace API { id: number; }; + type productcontrollerGetgroupedproductsParams = { + attribute?: string; + brand?: string; + }; + type productcontrollerGetproductbyidParams = { id: number; }; @@ -1371,15 +1437,25 @@ declare namespace API { }; type QueryOrderSalesDTO = { + /** 是否为原产品还是库存产品 */ isSource?: boolean; - exceptPackage?: boolean; /** 页码 */ current?: number; /** 每页大小 */ pageSize?: number; + /** 排序对象,格式如 { productName: "asc", sku: "desc" } */ + orderBy?: any; + /** 是否排除套餐 */ + exceptPackage?: boolean; + /** 站点ID */ siteId?: number; + /** 名称 */ name?: string; + /** SKU */ + sku?: string; + /** 开始日期 */ startDate?: string; + /** 结束日期 */ endDate?: string; }; @@ -3088,12 +3164,16 @@ declare namespace API { sku?: string; /** 分类ID (DictItem ID) */ categoryId?: number; + /** 分类名称 */ + categoryName?: string; /** 站点 SKU 列表 */ siteSkus?: any[]; /** 价格 */ price?: number; /** 促销价格 */ promotionPrice?: number; + /** 产品图片URL */ + image?: string; /** 属性列表 */ attributes?: any[]; /** 商品类型 */ @@ -3144,6 +3224,8 @@ declare namespace API { stockPointIds?: any; /** 站点网站URL */ websiteUrl?: string; + /** Webhook URL */ + webhookUrl?: string; }; type UpdateStockDTO = { -- 2.40.1 From 2ee8964bceb3c2412c03fedd139c42c18f43e788 Mon Sep 17 00:00:00 2001 From: tikkhun Date: Tue, 20 Jan 2026 17:19:02 +0800 Subject: [PATCH 2/6] =?UTF-8?q?feat(=E4=BA=A7=E5=93=81):=20=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E4=BA=A7=E5=93=81=E5=88=86=E7=BB=84=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E9=A1=B5=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 实现产品按属性分组展示功能,支持按分类和属性筛选 --- .umirc.ts | 4 +- src/pages/Product/GroupBy/index.tsx | 368 ++++++++++++++++++++++++++++ 2 files changed, 370 insertions(+), 2 deletions(-) create mode 100644 src/pages/Product/GroupBy/index.tsx diff --git a/.umirc.ts b/.umirc.ts index 0063d2b..5d47237 100644 --- a/.umirc.ts +++ b/.umirc.ts @@ -166,8 +166,8 @@ export default defineConfig({ }, { name: '产品品牌空间', - path: '/product/brandspace', - component: './Product/BrandSpace', + path: '/product/groupBy', + component: './Product/GroupBy', }, // sync { 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; -- 2.40.1 From d78b5870359737e7c80390f8da337305225aff3a Mon Sep 17 00:00:00 2001 From: tikkhun Date: Tue, 20 Jan 2026 17:20:41 +0800 Subject: [PATCH 3/6] =?UTF-8?q?refactor:=20=E6=B8=85=E7=90=86=E4=BB=A3?= =?UTF-8?q?=E7=A0=81=E6=A0=BC=E5=BC=8F=E5=B9=B6=E7=A7=BB=E9=99=A4=E6=9C=AA?= =?UTF-8?q?=E4=BD=BF=E7=94=A8=E7=9A=84BrandSpace=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit style: 统一字符串引号和代码缩进格式 fix: 修复订单同步日期范围参数格式 feat: 添加产品分组列表API接口 --- src/components/SyncForm.tsx | 46 +- src/pages/Order/List/index.tsx | 25 +- src/pages/Product/BrandSpace/index.tsx | 486 --------------------- src/pages/Product/CsvTool/index.tsx | 24 +- src/pages/Product/List/index.tsx | 5 +- src/pages/Site/List/index.tsx | 8 +- src/pages/Site/Shop/EditSiteForm.tsx | 1 - src/pages/Statistics/Order/index.tsx | 43 +- src/pages/Statistics/OrderSource/index.tsx | 18 +- src/servers/api/product.ts | 15 + src/servers/api/typings.d.ts | 53 +++ 11 files changed, 157 insertions(+), 567 deletions(-) delete mode 100644 src/pages/Product/BrandSpace/index.tsx 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={