refactor: 重构项目结构并优化代码
- 删除无用的文件和错误日志 - 创建统一的 imports 模块集中管理依赖 - 重构组件使用新的 imports 方式 - 修复文档路径大小写问题 - 优化类型定义和接口导出 - 更新依赖版本 - 改进错误处理和API配置 - 统一组件导出方式
This commit is contained in:
@@ -1,14 +1,33 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Card, Typography, Row, Col, Form, Input, Select, Button, Table, Statistic, Spin, message, Alert } from 'antd';
|
||||
import { CalculatorOutlined, SaveOutlined, HistoryOutlined, LineChartOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
useState,
|
||||
Link,
|
||||
Card,
|
||||
Typography,
|
||||
Row,
|
||||
Col,
|
||||
Form,
|
||||
Input,
|
||||
Select,
|
||||
Button,
|
||||
Table,
|
||||
Statistic,
|
||||
Spin,
|
||||
message,
|
||||
Alert,
|
||||
CalculatorOutlined,
|
||||
SaveOutlined,
|
||||
HistoryOutlined,
|
||||
LineChartOutlined,
|
||||
Title,
|
||||
Text,
|
||||
Paragraph,
|
||||
Option,
|
||||
Item,
|
||||
FC,
|
||||
} from '@/imports';
|
||||
|
||||
import { productDataSource } from '@/services/productDataSource';
|
||||
|
||||
const { Title, Text, Paragraph } = Typography;
|
||||
const { Option } = Select;
|
||||
const { Item } = Form;
|
||||
|
||||
interface PricingSuggestion {
|
||||
id: string;
|
||||
scenario: string;
|
||||
@@ -28,7 +47,7 @@ interface PriceHistory {
|
||||
operator: string;
|
||||
}
|
||||
|
||||
const AIPricing: React.FC = () => {
|
||||
const AIPricing: FC = () => {
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [pricingSuggestions, setPricingSuggestions] = useState<PricingSuggestion[]>([]);
|
||||
const [priceHistory, setPriceHistory] = useState<PriceHistory[]>([]);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
useState,
|
||||
useEffect,
|
||||
Card,
|
||||
Table,
|
||||
Button,
|
||||
@@ -24,8 +25,6 @@ import {
|
||||
Form,
|
||||
InputNumber,
|
||||
Switch,
|
||||
} from 'antd';
|
||||
import {
|
||||
SyncOutlined,
|
||||
EditOutlined,
|
||||
EyeOutlined,
|
||||
@@ -39,13 +38,14 @@ import {
|
||||
LinkOutlined,
|
||||
ArrowUpOutlined,
|
||||
ArrowDownOutlined,
|
||||
} from '@ant-design/icons';
|
||||
Text,
|
||||
Title,
|
||||
Option,
|
||||
Search,
|
||||
FC,
|
||||
} from '@/imports';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
|
||||
const { Text, Title } = Typography;
|
||||
const { Option } = Select;
|
||||
const { Search } = Input;
|
||||
|
||||
|
||||
// ==================== 多商户店铺配置 ====================
|
||||
// 当前用户拥有的店铺(根据登录用户的权限动态加载)
|
||||
@@ -302,7 +302,7 @@ const STATUS_CONFIG: Record<string, { color: string; text: string; icon: React.R
|
||||
NOT_LISTED: { color: 'default', text: '未上架', icon: <CloseCircleOutlined /> },
|
||||
};
|
||||
|
||||
const CrossPlatformManage: React.FC = () => {
|
||||
const CrossPlatformManage: FC = () => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [products, setProducts] = useState<CrossPlatformProduct[]>([]);
|
||||
const [filteredProducts, setFilteredProducts] = useState<CrossPlatformProduct[]>([]);
|
||||
@@ -666,7 +666,7 @@ const CrossPlatformManage: React.FC = () => {
|
||||
{/* 商品详情弹窗 */}
|
||||
<Modal
|
||||
title="商品详情"
|
||||
visible={detailModalVisible}
|
||||
open={detailModalVisible}
|
||||
onCancel={() => setDetailModalVisible(false)}
|
||||
width={900}
|
||||
footer={[
|
||||
@@ -780,7 +780,7 @@ const CrossPlatformManage: React.FC = () => {
|
||||
{/* 同步弹窗 */}
|
||||
<Modal
|
||||
title="同步商品"
|
||||
visible={syncModalVisible}
|
||||
open={syncModalVisible}
|
||||
onCancel={() => setSyncModalVisible(false)}
|
||||
onOk={handleSync}
|
||||
width={600}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
useState,
|
||||
Card,
|
||||
Upload,
|
||||
Button,
|
||||
@@ -18,8 +18,6 @@ import {
|
||||
Tooltip,
|
||||
Alert,
|
||||
Tabs,
|
||||
} from 'antd';
|
||||
import {
|
||||
UploadOutlined,
|
||||
DeleteOutlined,
|
||||
EyeOutlined,
|
||||
@@ -31,13 +29,13 @@ import {
|
||||
ReloadOutlined,
|
||||
SearchOutlined,
|
||||
FilterOutlined,
|
||||
} from '@ant-design/icons';
|
||||
Option,
|
||||
FC,
|
||||
} from '@/imports';
|
||||
import type { UploadFile } from 'antd/es/upload/interface';
|
||||
import { productDataSource } from '@/services/productDataSource';
|
||||
import { Product } from '@/types/product';
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
interface MaterialFile {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -128,7 +126,7 @@ const formatFileSize = (bytes: number): string => {
|
||||
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
|
||||
};
|
||||
|
||||
export const MaterialUpload: React.FC = () => {
|
||||
const MaterialUpload: FC = () => {
|
||||
const [fileList, setFileList] = useState<UploadFile[]>([]);
|
||||
const [materials, setMaterials] = useState<MaterialFile[]>(MOCK_MATERIALS);
|
||||
const [uploading, setUploading] = useState(false);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
useState,
|
||||
useEffect,
|
||||
Card,
|
||||
Descriptions,
|
||||
Button,
|
||||
@@ -17,8 +18,6 @@ import {
|
||||
Typography,
|
||||
Tooltip,
|
||||
Badge,
|
||||
} from 'antd';
|
||||
import {
|
||||
ShoppingCartOutlined,
|
||||
EditOutlined,
|
||||
DeleteOutlined,
|
||||
@@ -32,15 +31,15 @@ import {
|
||||
CheckCircleOutlined,
|
||||
CloseCircleOutlined,
|
||||
SyncOutlined,
|
||||
} from '@ant-design/icons';
|
||||
Title,
|
||||
Text,
|
||||
FC,
|
||||
} from '@/imports';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
import { productDataSource } from '@/services/productDataSource';
|
||||
import { Product } from '@/types/product';
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
|
||||
|
||||
interface ProductDetail {
|
||||
interface ProductDetailData {
|
||||
id: string;
|
||||
productId: string;
|
||||
platform: string;
|
||||
@@ -155,9 +154,9 @@ const COMPLIANCE_CONFIG: Record<string, { color: string; icon: React.ReactNode }
|
||||
PENDING_REVIEW: { color: 'warning', icon: <SyncOutlined spin /> },
|
||||
};
|
||||
|
||||
export const ProductDetail: React.FC = () => {
|
||||
const ProductDetail: FC = () => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [product, setProduct] = useState<ProductDetail | null>(null);
|
||||
const [product, setProduct] = useState<ProductDetailData | null>(null);
|
||||
const [priceHistory, setPriceHistory] = useState<PriceHistory[]>([]);
|
||||
const [stockHistory, setStockHistory] = useState<StockHistory[]>([]);
|
||||
const [editModalVisible, setEditModalVisible] = useState(false);
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
useState,
|
||||
useEffect,
|
||||
useCallback,
|
||||
useMemo,
|
||||
useNavigate,
|
||||
Card,
|
||||
Table,
|
||||
Button,
|
||||
@@ -26,8 +29,6 @@ import {
|
||||
Spin,
|
||||
Tabs,
|
||||
Statistic,
|
||||
} from 'antd';
|
||||
import {
|
||||
PlusOutlined,
|
||||
EditOutlined,
|
||||
DeleteOutlined,
|
||||
@@ -50,66 +51,27 @@ import {
|
||||
GlobalOutlined,
|
||||
ShopOutlined,
|
||||
AppstoreOutlined,
|
||||
} from '@ant-design/icons';
|
||||
Title,
|
||||
Text,
|
||||
Option,
|
||||
RangePicker,
|
||||
Search,
|
||||
ColumnsType,
|
||||
TablePaginationConfig,
|
||||
FilterValue,
|
||||
SorterResult,
|
||||
TableCurrentDataSource,
|
||||
moment,
|
||||
productDataSource,
|
||||
Product,
|
||||
Shop,
|
||||
ProductFilter,
|
||||
ProductSort,
|
||||
PLATFORMS,
|
||||
} from '@/imports';
|
||||
import { LoadingState, EmptyState } from '../../components/ui';
|
||||
|
||||
import type { ColumnsType, TablePaginationConfig } from 'antd/es/table';
|
||||
import type { FilterValue, SorterResult, TableCurrentDataSource } from 'antd/es/table/interface';
|
||||
import moment from 'moment';
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
const { Option } = Select;
|
||||
const { RangePicker } = DatePicker;
|
||||
const { Search } = Input;
|
||||
|
||||
interface Product {
|
||||
id: string;
|
||||
sku: string;
|
||||
name: string;
|
||||
image: string;
|
||||
category: string;
|
||||
price: number;
|
||||
costPrice: number;
|
||||
profit: number;
|
||||
roi: number;
|
||||
stock: number;
|
||||
status: 'DRAFT' | 'PRICED' | 'LISTED' | 'SYNCING' | 'LIVE' | 'SYNC_FAILED' | 'OFFLINE';
|
||||
platformStatus: Record<string, string>;
|
||||
shopId: string;
|
||||
shopName: string;
|
||||
platform: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
interface Shop {
|
||||
id: string;
|
||||
name: string;
|
||||
platform: string;
|
||||
status: 'ACTIVE' | 'INACTIVE' | 'SUSPENDED';
|
||||
region: string;
|
||||
currency: string;
|
||||
}
|
||||
|
||||
interface FilterState {
|
||||
keyword: string;
|
||||
status: string[];
|
||||
platform: string[];
|
||||
shop: string[];
|
||||
category: string[];
|
||||
priceRange: [number, number] | null;
|
||||
stockRange: [number, number] | null;
|
||||
roiRange: [number, number] | null;
|
||||
dateRange: any;
|
||||
minProfit: number | null;
|
||||
minStock: number | null;
|
||||
searchInDescription: boolean;
|
||||
}
|
||||
|
||||
interface SortState {
|
||||
field: string;
|
||||
order: 'ascend' | 'descend' | null;
|
||||
}
|
||||
const CATEGORIES = ['工业自动化', '电子元器件', '工具设备', '仪器仪表', '安防设备'];
|
||||
|
||||
const STATUS_CONFIG: Record<string, { color: string; text: string; icon: React.ReactNode }> = {
|
||||
DRAFT: { color: 'default', text: '草稿', icon: <EditOutlined /> },
|
||||
@@ -121,86 +83,6 @@ const STATUS_CONFIG: Record<string, { color: string; text: string; icon: React.R
|
||||
OFFLINE: { color: 'default', text: '已下架', icon: <ExclamationCircleOutlined /> },
|
||||
};
|
||||
|
||||
const PLATFORMS = [
|
||||
// TikTok系列
|
||||
'TikTok', 'TikTokFull',
|
||||
|
||||
// Shopee系列
|
||||
'Shopee', 'ShopeeFull', 'ShopeeLight',
|
||||
|
||||
// Lazada系列
|
||||
'Lazada', 'LazadaFull',
|
||||
|
||||
// Temu
|
||||
'TemuFull',
|
||||
|
||||
// SHEIN系列
|
||||
'Shein', 'SheinHalf',
|
||||
|
||||
// 其他平台
|
||||
'Ozon', 'Yandex', 'AliExpress', 'AliExpressHalf', 'AliExpressPop',
|
||||
'Coupang', 'Walmart', 'Wildberries', 'Allegro', 'MercadoLibre',
|
||||
'Jumia', 'Joom', 'Amazon', 'Wish', 'Emag', 'Miravia',
|
||||
'Daraz', 'Joybuy', 'Alibaba', 'Qoo10', 'Shopify', 'Shoplazza',
|
||||
'ShopyyV1', 'ShopyyV2', 'Shopline', 'GreatBoss', 'Other',
|
||||
|
||||
// 原有平台
|
||||
'eBay',
|
||||
];
|
||||
const CATEGORIES = ['工业自动化', '电子元器件', '工具设备', '仪器仪表', '安防设备'];
|
||||
|
||||
// 店铺数据
|
||||
const SHOPS: Shop[] = [
|
||||
// TikTok平台 - 多个店铺
|
||||
{ id: 'shop-tiktok-1', name: 'TikTok旗舰店A', platform: 'TikTok', status: 'ACTIVE', region: '中国', currency: 'CNY' },
|
||||
{ id: 'shop-tiktok-2', name: 'TikTok旗舰店B', platform: 'TikTok', status: 'ACTIVE', region: '美国', currency: 'USD' },
|
||||
{ id: 'shop-tiktok-3', name: 'TikTok精品店C', platform: 'TikTok', status: 'ACTIVE', region: '欧洲', currency: 'EUR' },
|
||||
{ id: 'shop-tiktok-4', name: 'TikTok精品店D', platform: 'TikTok', status: 'INACTIVE', region: '东南亚', currency: 'SGD' },
|
||||
|
||||
// Shopee平台 - 多个店铺
|
||||
{ id: 'shop-shopee-1', name: 'Shopee官方店A', platform: 'Shopee', status: 'ACTIVE', region: '东南亚', currency: 'SGD' },
|
||||
{ id: 'shop-shopee-2', name: 'Shopee官方店B', platform: 'Shopee', status: 'ACTIVE', region: '台湾', currency: 'TWD' },
|
||||
{ id: 'shop-shopee-3', name: 'Shopee旗舰店C', platform: 'Shopee', status: 'ACTIVE', region: '马来西亚', currency: 'MYR' },
|
||||
{ id: 'shop-shopee-4', name: 'Shopee旗舰店D', platform: 'Shopee', status: 'SUSPENDED', region: '菲律宾', currency: 'PHP' },
|
||||
|
||||
// Lazada平台 - 多个店铺
|
||||
{ id: 'shop-lazada-1', name: 'Lazada官方店A', platform: 'Lazada', status: 'ACTIVE', region: '东南亚', currency: 'SGD' },
|
||||
{ id: 'shop-lazada-2', name: 'Lazada官方店B', platform: 'Lazada', status: 'ACTIVE', region: '泰国', currency: 'THB' },
|
||||
{ id: 'shop-lazada-3', name: 'Lazada旗舰店C', platform: 'Lazada', status: 'ACTIVE', region: '越南', currency: 'VND' },
|
||||
|
||||
// Amazon平台 - 多个店铺
|
||||
{ id: 'shop-amazon-1', name: 'Amazon美国店A', platform: 'Amazon', status: 'ACTIVE', region: '美国', currency: 'USD' },
|
||||
{ id: 'shop-amazon-2', name: 'Amazon美国店B', platform: 'Amazon', status: 'ACTIVE', region: '美国', currency: 'USD' },
|
||||
{ id: 'shop-amazon-3', name: 'Amazon欧洲店C', platform: 'Amazon', status: 'ACTIVE', region: '欧洲', currency: 'EUR' },
|
||||
{ id: 'shop-amazon-4', name: 'Amazon欧洲店D', platform: 'Amazon', status: 'ACTIVE', region: '英国', currency: 'GBP' },
|
||||
{ id: 'shop-amazon-5', name: 'Amazon日本店E', platform: 'Amazon', status: 'ACTIVE', region: '日本', currency: 'JPY' },
|
||||
|
||||
// eBay平台 - 多个店铺
|
||||
{ id: 'shop-ebay-1', name: 'eBay全球店A', platform: 'eBay', status: 'ACTIVE', region: '全球', currency: 'USD' },
|
||||
{ id: 'shop-ebay-2', name: 'eBay全球店B', platform: 'eBay', status: 'ACTIVE', region: '欧洲', currency: 'EUR' },
|
||||
{ id: 'shop-ebay-3', name: 'eBay美国店C', platform: 'eBay', status: 'ACTIVE', region: '美国', currency: 'USD' },
|
||||
|
||||
// Shopify平台 - 多个店铺
|
||||
{ id: 'shop-shopify-1', name: 'Shopify独立站A', platform: 'Shopify', status: 'ACTIVE', region: '全球', currency: 'USD' },
|
||||
{ id: 'shop-shopify-2', name: 'Shopify独立站B', platform: 'Shopify', status: 'ACTIVE', region: '欧洲', currency: 'EUR' },
|
||||
{ id: 'shop-shopify-3', name: 'Shopify独立站C', platform: 'Shopify', status: 'ACTIVE', region: '亚洲', currency: 'CNY' },
|
||||
|
||||
// Temu平台 - 多个店铺
|
||||
{ id: 'shop-temu-1', name: 'Temu旗舰店A', platform: 'Temu', status: 'ACTIVE', region: '美国', currency: 'USD' },
|
||||
{ id: 'shop-temu-2', name: 'Temu旗舰店B', platform: 'Temu', status: 'ACTIVE', region: '欧洲', currency: 'EUR' },
|
||||
{ id: 'shop-temu-3', name: 'Temu旗舰店C', platform: 'Temu', status: 'ACTIVE', region: '澳大利亚', currency: 'AUD' },
|
||||
|
||||
// SHEIN平台 - 多个店铺
|
||||
{ id: 'shop-shein-1', name: 'SHEIN官方店A', platform: 'Shein', status: 'ACTIVE', region: '全球', currency: 'USD' },
|
||||
{ id: 'shop-shein-2', name: 'SHEIN官方店B', platform: 'Shein', status: 'ACTIVE', region: '欧洲', currency: 'EUR' },
|
||||
{ id: 'shop-shein-3', name: 'SHEIN官方店C', platform: 'Shein', status: 'ACTIVE', region: '中东', currency: 'AED' },
|
||||
|
||||
// 其他平台店铺
|
||||
{ id: 'shop-ozon-1', name: 'Ozon旗舰店A', platform: 'Ozon', status: 'ACTIVE', region: '俄罗斯', currency: 'RUB' },
|
||||
{ id: 'shop-ali-1', name: '速卖通旗舰店A', platform: 'AliExpress', status: 'ACTIVE', region: '全球', currency: 'USD' },
|
||||
{ id: 'shop-walmart-1', name: 'Walmart旗舰店A', platform: 'Walmart', status: 'ACTIVE', region: '美国', currency: 'USD' },
|
||||
];
|
||||
|
||||
const PLATFORM_CONFIG: Record<string, { icon: React.ReactNode; color: string }> = {
|
||||
// TikTok系列
|
||||
TikTok: { icon: <GlobalOutlined />, color: '#000000' },
|
||||
@@ -255,143 +137,7 @@ const PLATFORM_CONFIG: Record<string, { icon: React.ReactNode; color: string }>
|
||||
eBay: { icon: <GlobalOutlined />, color: '#e53238' },
|
||||
};
|
||||
|
||||
const MOCK_PRODUCTS: Product[] = [
|
||||
{
|
||||
id: '1',
|
||||
sku: 'TP-TEMP-001',
|
||||
name: '工业温度传感器 Pro',
|
||||
image: 'https://via.placeholder.com/80x80?text=Product',
|
||||
category: '工业自动化',
|
||||
price: 89.99,
|
||||
costPrice: 45.00,
|
||||
profit: 44.99,
|
||||
roi: 99.98,
|
||||
stock: 256,
|
||||
status: 'LIVE',
|
||||
platformStatus: { Amazon: 'LIVE', eBay: 'LIVE', Shopee: 'PENDING' },
|
||||
shopId: 'shop-amazon-1',
|
||||
shopName: 'Amazon美国店',
|
||||
platform: 'Amazon',
|
||||
createdAt: '2025-12-15',
|
||||
updatedAt: '2026-03-18',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
sku: 'TP-PRES-002',
|
||||
name: '压力传感器 Digital',
|
||||
image: 'https://via.placeholder.com/80x80?text=Product',
|
||||
category: '工业自动化',
|
||||
price: 129.99,
|
||||
costPrice: 65.00,
|
||||
profit: 64.99,
|
||||
roi: 99.98,
|
||||
stock: 128,
|
||||
status: 'DRAFT',
|
||||
platformStatus: {},
|
||||
shopId: 'shop-tiktok-1',
|
||||
shopName: 'TikTok旗舰店',
|
||||
platform: 'TikTok',
|
||||
createdAt: '2026-03-10',
|
||||
updatedAt: '2026-03-10',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
sku: 'TP-FLOW-003',
|
||||
name: '流量计 Ultra',
|
||||
image: 'https://via.placeholder.com/80x80?text=Product',
|
||||
category: '仪器仪表',
|
||||
price: 299.99,
|
||||
costPrice: 150.00,
|
||||
profit: 149.99,
|
||||
roi: 99.99,
|
||||
stock: 64,
|
||||
status: 'PRICED',
|
||||
platformStatus: {},
|
||||
shopId: 'shop-shopee-1',
|
||||
shopName: 'Shopee官方店',
|
||||
platform: 'Shopee',
|
||||
createdAt: '2026-03-15',
|
||||
updatedAt: '2026-03-16',
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
sku: 'TP-MOTR-004',
|
||||
name: '步进电机 57型',
|
||||
image: 'https://via.placeholder.com/80x80?text=Product',
|
||||
category: '工业自动化',
|
||||
price: 59.99,
|
||||
costPrice: 30.00,
|
||||
profit: 29.99,
|
||||
roi: 99.97,
|
||||
stock: 512,
|
||||
status: 'LISTED',
|
||||
platformStatus: { Amazon: 'LISTED' },
|
||||
shopId: 'shop-amazon-2',
|
||||
shopName: 'Amazon欧洲店',
|
||||
platform: 'Amazon',
|
||||
createdAt: '2026-03-01',
|
||||
updatedAt: '2026-03-17',
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
sku: 'TP-CTRL-005',
|
||||
name: 'PLC控制器 Mini',
|
||||
image: 'https://via.placeholder.com/80x80?text=Product',
|
||||
category: '工业自动化',
|
||||
price: 199.99,
|
||||
costPrice: 100.00,
|
||||
profit: 99.99,
|
||||
roi: 99.99,
|
||||
stock: 32,
|
||||
status: 'SYNCING',
|
||||
platformStatus: { Amazon: 'SYNCING', eBay: 'SYNCING' },
|
||||
shopId: 'shop-ebay-1',
|
||||
shopName: 'eBay全球店',
|
||||
platform: 'eBay',
|
||||
createdAt: '2026-03-18',
|
||||
updatedAt: '2026-03-18',
|
||||
},
|
||||
{
|
||||
id: '6',
|
||||
sku: 'TP-SENS-006',
|
||||
name: '光电传感器',
|
||||
image: 'https://via.placeholder.com/80x80?text=Product',
|
||||
category: '工业自动化',
|
||||
price: 39.99,
|
||||
costPrice: 20.00,
|
||||
profit: 19.99,
|
||||
roi: 99.95,
|
||||
stock: 0,
|
||||
status: 'SYNC_FAILED',
|
||||
platformStatus: { Amazon: 'FAILED' },
|
||||
shopId: 'shop-temu-1',
|
||||
shopName: 'Temu旗舰店',
|
||||
platform: 'Temu',
|
||||
createdAt: '2026-03-05',
|
||||
updatedAt: '2026-03-18',
|
||||
},
|
||||
{
|
||||
id: '7',
|
||||
sku: 'TP-VALV-007',
|
||||
name: '电磁阀 24V',
|
||||
image: 'https://via.placeholder.com/80x80?text=Product',
|
||||
category: '工具设备',
|
||||
price: 79.99,
|
||||
costPrice: 40.00,
|
||||
profit: 39.99,
|
||||
roi: 99.98,
|
||||
stock: 200,
|
||||
status: 'OFFLINE',
|
||||
platformStatus: { Amazon: 'OFFLINE', eBay: 'OFFLINE' },
|
||||
shopId: 'shop-shein-1',
|
||||
shopName: 'SHEIN官方店',
|
||||
platform: 'Shein',
|
||||
createdAt: '2025-11-20',
|
||||
updatedAt: '2026-02-28',
|
||||
},
|
||||
];
|
||||
|
||||
export const ProductList: React.FC = () => {
|
||||
const ProductList: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const [products, setProducts] = useState<Product[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -403,23 +149,28 @@ export const ProductList: React.FC = () => {
|
||||
const [currentProduct, setCurrentProduct] = useState<Product | null>(null);
|
||||
const [syncLoading, setSyncLoading] = useState<Record<string, boolean>>({});
|
||||
const [activePlatformTab, setActivePlatformTab] = useState<string>('all');
|
||||
const [shops, setShops] = useState<Shop[]>([]);
|
||||
const [platformStats, setPlatformStats] = useState<Record<string, { total: number; live: number; pending: number; failed: number }>>({
|
||||
all: { total: 0, live: 0, pending: 0, failed: 0 },
|
||||
unpublished: { total: 0, live: 0, pending: 0, failed: 0 },
|
||||
});
|
||||
|
||||
const [filters, setFilters] = useState<FilterState>({
|
||||
const [filters, setFilters] = useState<ProductFilter>({
|
||||
keyword: '',
|
||||
status: [],
|
||||
platform: [],
|
||||
shop: [],
|
||||
category: [],
|
||||
priceRange: null,
|
||||
stockRange: null,
|
||||
roiRange: null,
|
||||
dateRange: null,
|
||||
minProfit: null,
|
||||
minStock: null,
|
||||
priceRange: undefined,
|
||||
stockRange: undefined,
|
||||
roiRange: undefined,
|
||||
dateRange: undefined,
|
||||
minProfit: undefined,
|
||||
minStock: undefined,
|
||||
searchInDescription: false,
|
||||
});
|
||||
|
||||
const [sort, setSort] = useState<SortState>({
|
||||
const [sort, setSort] = useState<ProductSort>({
|
||||
field: 'updatedAt',
|
||||
order: 'descend',
|
||||
});
|
||||
@@ -432,17 +183,51 @@ export const ProductList: React.FC = () => {
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchProducts();
|
||||
fetchInitialData();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchProducts();
|
||||
}, [filters, sort, activePlatformTab]);
|
||||
|
||||
const fetchInitialData = async () => {
|
||||
try {
|
||||
const [shopsData, stats] = await Promise.all([
|
||||
productDataSource.fetchShops(),
|
||||
productDataSource.getPlatformStats()
|
||||
]);
|
||||
setShops(shopsData);
|
||||
setPlatformStats(stats);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch initial data:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchProducts = useCallback(async () => {
|
||||
setLoading(true);
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
setProducts(MOCK_PRODUCTS);
|
||||
setLoading(false);
|
||||
}, []);
|
||||
try {
|
||||
const productFilter: ProductFilter = {
|
||||
...filters,
|
||||
platform: activePlatformTab !== 'all' && activePlatformTab !== 'unpublished'
|
||||
? [activePlatformTab]
|
||||
: filters.platform
|
||||
};
|
||||
|
||||
const productsData = await productDataSource.fetchProducts(productFilter, sort);
|
||||
setProducts(productsData);
|
||||
|
||||
// 更新平台统计数据
|
||||
const stats = await productDataSource.getPlatformStats();
|
||||
setPlatformStats(stats);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch products:', error);
|
||||
message.error('Failed to load products');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [filters, sort, activePlatformTab]);
|
||||
|
||||
const handleFilterChange = (key: keyof FilterState, value: any) => {
|
||||
const handleFilterChange = (key: keyof ProductFilter, value: any) => {
|
||||
setFilters(prev => ({ ...prev, [key]: value }));
|
||||
};
|
||||
|
||||
@@ -477,12 +262,12 @@ export const ProductList: React.FC = () => {
|
||||
platform: [],
|
||||
shop: [],
|
||||
category: [],
|
||||
priceRange: null,
|
||||
stockRange: null,
|
||||
roiRange: null,
|
||||
dateRange: null,
|
||||
minProfit: null,
|
||||
minStock: null,
|
||||
priceRange: undefined,
|
||||
stockRange: undefined,
|
||||
roiRange: undefined,
|
||||
dateRange: undefined,
|
||||
minProfit: undefined,
|
||||
minStock: undefined,
|
||||
searchInDescription: false,
|
||||
});
|
||||
message.success('筛选条件已重置');
|
||||
@@ -512,32 +297,36 @@ export const ProductList: React.FC = () => {
|
||||
navigate(`/dashboard/product/detail/${record.id}`);
|
||||
};
|
||||
|
||||
const handleDeleteProduct = (record: Product) => {
|
||||
const handleDeleteProduct = async (record: Product) => {
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: `确定要删除商品 "${record.name}" 吗?此操作不可恢复。`,
|
||||
okText: '删除',
|
||||
okType: 'danger',
|
||||
onOk: () => {
|
||||
setProducts(products.filter(p => p.id !== record.id));
|
||||
message.success('商品删除成功');
|
||||
onOk: async () => {
|
||||
try {
|
||||
const success = await productDataSource.deleteProduct(record.id);
|
||||
if (success) {
|
||||
message.success('商品删除成功');
|
||||
fetchProducts();
|
||||
} else {
|
||||
message.error('删除失败');
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('删除失败');
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleDuplicateProduct = (record: Product) => {
|
||||
const newProduct: Product = {
|
||||
...record,
|
||||
id: `${Date.now()}`,
|
||||
sku: `${record.sku}-COPY`,
|
||||
name: `${record.name} (复制)`,
|
||||
status: 'DRAFT',
|
||||
platformStatus: {},
|
||||
createdAt: moment().format('YYYY-MM-DD'),
|
||||
updatedAt: moment().format('YYYY-MM-DD'),
|
||||
};
|
||||
setProducts([newProduct, ...products]);
|
||||
message.success('商品复制成功');
|
||||
const handleDuplicateProduct = async (record: Product) => {
|
||||
try {
|
||||
await productDataSource.duplicateProduct(record.id);
|
||||
message.success('商品复制成功');
|
||||
fetchProducts();
|
||||
} catch (error) {
|
||||
message.error('复制失败');
|
||||
}
|
||||
};
|
||||
|
||||
const handlePricing = (record: Product) => {
|
||||
@@ -545,13 +334,18 @@ export const ProductList: React.FC = () => {
|
||||
setPricingModalVisible(true);
|
||||
};
|
||||
|
||||
const handlePublish = (record: Product) => {
|
||||
const handlePublish = async (record: Product) => {
|
||||
Modal.confirm({
|
||||
title: '确认上架',
|
||||
content: `确定要上架商品 "${record.name}" 吗?`,
|
||||
onOk: () => {
|
||||
updateProductStatus(record.id, 'LISTED');
|
||||
message.success('商品上架成功');
|
||||
onOk: async () => {
|
||||
try {
|
||||
await productDataSource.updateProductStatus(record.id, 'LISTED');
|
||||
message.success('商品上架成功');
|
||||
fetchProducts();
|
||||
} catch (error) {
|
||||
message.error('上架失败');
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -559,30 +353,31 @@ export const ProductList: React.FC = () => {
|
||||
const handleSync = async (record: Product) => {
|
||||
setSyncLoading(prev => ({ ...prev, [record.id]: true }));
|
||||
|
||||
updateProductStatus(record.id, 'SYNCING');
|
||||
message.loading({ content: '正在同步到平台...', key: record.id });
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
|
||||
const success = Math.random() > 0.3;
|
||||
if (success) {
|
||||
updateProductStatus(record.id, 'LIVE');
|
||||
message.success({ content: '同步成功', key: record.id });
|
||||
} else {
|
||||
updateProductStatus(record.id, 'SYNC_FAILED');
|
||||
try {
|
||||
await productDataSource.syncProduct(record.id);
|
||||
message.success({ content: '同步完成', key: record.id });
|
||||
fetchProducts();
|
||||
} catch (error) {
|
||||
message.error({ content: '同步失败,请重试', key: record.id });
|
||||
} finally {
|
||||
setSyncLoading(prev => ({ ...prev, [record.id]: false }));
|
||||
}
|
||||
|
||||
setSyncLoading(prev => ({ ...prev, [record.id]: false }));
|
||||
};
|
||||
|
||||
const handleOffline = (record: Product) => {
|
||||
const handleOffline = async (record: Product) => {
|
||||
Modal.confirm({
|
||||
title: '确认下架',
|
||||
content: `确定要下架商品 "${record.name}" 吗?`,
|
||||
onOk: () => {
|
||||
updateProductStatus(record.id, 'OFFLINE');
|
||||
message.success('商品已下架');
|
||||
onOk: async () => {
|
||||
try {
|
||||
await productDataSource.updateProductStatus(record.id, 'OFFLINE');
|
||||
message.success('商品已下架');
|
||||
fetchProducts();
|
||||
} catch (error) {
|
||||
message.error('下架失败');
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -591,12 +386,6 @@ export const ProductList: React.FC = () => {
|
||||
handleSync(record);
|
||||
};
|
||||
|
||||
const updateProductStatus = (productId: string, status: Product['status']) => {
|
||||
setProducts(products.map(p =>
|
||||
p.id === productId ? { ...p, status, updatedAt: moment().format('YYYY-MM-DD') } : p
|
||||
));
|
||||
};
|
||||
|
||||
const handleBatchDelete = () => {
|
||||
if (selectedRows.length === 0) {
|
||||
message.warning('请先选择要删除的商品');
|
||||
@@ -607,11 +396,16 @@ export const ProductList: React.FC = () => {
|
||||
content: `确定要删除选中的 ${selectedRows.length} 个商品吗?`,
|
||||
okText: '删除',
|
||||
okType: 'danger',
|
||||
onOk: () => {
|
||||
const ids = selectedRows.map(r => r.id);
|
||||
setProducts(products.filter(p => !ids.includes(p.id)));
|
||||
setSelectedRows([]);
|
||||
message.success(`成功删除 ${ids.length} 个商品`);
|
||||
onOk: async () => {
|
||||
try {
|
||||
const promises = selectedRows.map(r => productDataSource.deleteProduct(r.id));
|
||||
await Promise.all(promises);
|
||||
message.success(`成功删除 ${selectedRows.length} 个商品`);
|
||||
setSelectedRows([]);
|
||||
fetchProducts();
|
||||
} catch (error) {
|
||||
message.error('批量删除失败');
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -624,7 +418,7 @@ export const ProductList: React.FC = () => {
|
||||
message.info(`批量定价功能:已选择 ${selectedRows.length} 个商品`);
|
||||
};
|
||||
|
||||
const handleBatchPublish = () => {
|
||||
const handleBatchPublish = async () => {
|
||||
if (selectedRows.length === 0) {
|
||||
message.warning('请先选择要上架的商品');
|
||||
return;
|
||||
@@ -637,15 +431,21 @@ export const ProductList: React.FC = () => {
|
||||
Modal.confirm({
|
||||
title: '确认批量上架',
|
||||
content: `确定要上架选中的 ${pricedProducts.length} 个商品吗?`,
|
||||
onOk: () => {
|
||||
pricedProducts.forEach(p => updateProductStatus(p.id, 'LISTED'));
|
||||
setSelectedRows([]);
|
||||
message.success(`成功上架 ${pricedProducts.length} 个商品`);
|
||||
onOk: async () => {
|
||||
try {
|
||||
const promises = pricedProducts.map(p => productDataSource.updateProductStatus(p.id, 'LISTED'));
|
||||
await Promise.all(promises);
|
||||
message.success(`成功上架 ${pricedProducts.length} 个商品`);
|
||||
setSelectedRows([]);
|
||||
fetchProducts();
|
||||
} catch (error) {
|
||||
message.error('批量上架失败');
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleBatchSync = () => {
|
||||
const handleBatchSync = async () => {
|
||||
if (selectedRows.length === 0) {
|
||||
message.warning('请先选择要同步的商品');
|
||||
return;
|
||||
@@ -656,10 +456,15 @@ export const ProductList: React.FC = () => {
|
||||
return;
|
||||
}
|
||||
message.loading('正在批量同步...');
|
||||
setTimeout(() => {
|
||||
listableProducts.forEach(p => handleSync(p));
|
||||
try {
|
||||
const promises = listableProducts.map(p => productDataSource.syncProduct(p.id));
|
||||
await Promise.all(promises);
|
||||
message.success('批量同步完成');
|
||||
setSelectedRows([]);
|
||||
}, 500);
|
||||
fetchProducts();
|
||||
} catch (error) {
|
||||
message.error('批量同步失败');
|
||||
}
|
||||
};
|
||||
|
||||
const handleBatchUpdate = () => {
|
||||
@@ -680,109 +485,7 @@ export const ProductList: React.FC = () => {
|
||||
// 这里可以实现导出商品数据为Excel或CSV文件
|
||||
};
|
||||
|
||||
const filteredProducts = products.filter(product => {
|
||||
if (filters.keyword) {
|
||||
const keywordLower = filters.keyword.toLowerCase();
|
||||
if (!product.name.toLowerCase().includes(keywordLower) &&
|
||||
!product.sku.toLowerCase().includes(keywordLower)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (filters.status.length > 0 && !filters.status.includes(product.status)) {
|
||||
return false;
|
||||
}
|
||||
if (filters.category.length > 0 && !filters.category.includes(product.category)) {
|
||||
return false;
|
||||
}
|
||||
if (filters.shop.length > 0 && !filters.shop.includes(product.shopId)) {
|
||||
return false;
|
||||
}
|
||||
if (filters.priceRange) {
|
||||
const [min, max] = filters.priceRange;
|
||||
if (product.price < min || product.price > max) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (filters.stockRange) {
|
||||
const [min, max] = filters.stockRange;
|
||||
if (product.stock < min || product.stock > max) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (filters.roiRange) {
|
||||
const [min, max] = filters.roiRange;
|
||||
if (product.roi < min || product.roi > max) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (filters.minProfit !== null && product.profit < filters.minProfit) {
|
||||
return false;
|
||||
}
|
||||
if (filters.minStock !== null && product.stock < filters.minStock) {
|
||||
return false;
|
||||
}
|
||||
if (activePlatformTab !== 'all') {
|
||||
if (activePlatformTab === 'unpublished') {
|
||||
if (Object.keys(product.platformStatus).length > 0) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
if (!product.platformStatus[activePlatformTab]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
const platformStats = useMemo(() => {
|
||||
const stats: Record<string, { total: number; live: number; pending: number; failed: number }> = {
|
||||
all: { total: products.length, live: 0, pending: 0, failed: 0 },
|
||||
unpublished: { total: 0, live: 0, pending: 0, failed: 0 },
|
||||
};
|
||||
|
||||
PLATFORMS.forEach(platform => {
|
||||
stats[platform] = { total: 0, live: 0, pending: 0, failed: 0 };
|
||||
});
|
||||
|
||||
products.forEach(product => {
|
||||
const platforms = Object.keys(product.platformStatus);
|
||||
if (platforms.length === 0) {
|
||||
stats.unpublished.total++;
|
||||
}
|
||||
platforms.forEach(platform => {
|
||||
if (!stats[platform]) {
|
||||
stats[platform] = { total: 0, live: 0, pending: 0, failed: 0 };
|
||||
}
|
||||
stats[platform].total++;
|
||||
const status = product.platformStatus[platform];
|
||||
if (status === 'LIVE') {
|
||||
stats[platform].live++;
|
||||
stats.all.live++;
|
||||
} else if (status === 'PENDING' || status === 'SYNCING' || status === 'LISTED') {
|
||||
stats[platform].pending++;
|
||||
stats.all.pending++;
|
||||
} else if (status === 'FAILED' || status === 'SYNC_FAILED') {
|
||||
stats[platform].failed++;
|
||||
stats.all.failed++;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return stats;
|
||||
}, [products]);
|
||||
|
||||
const sortedProducts = [...filteredProducts].sort((a, b) => {
|
||||
const field = sort.field as keyof Product;
|
||||
const aValue = a[field];
|
||||
const bValue = b[field];
|
||||
|
||||
if (sort.order === 'ascend') {
|
||||
return aValue > bValue ? 1 : -1;
|
||||
} else {
|
||||
return aValue < bValue ? 1 : -1;
|
||||
}
|
||||
});
|
||||
const sortedProducts = products;
|
||||
|
||||
const allColumns: ColumnsType<Product> = [
|
||||
{
|
||||
@@ -1171,7 +874,7 @@ export const ProductList: React.FC = () => {
|
||||
title="筛选条件"
|
||||
placement="right"
|
||||
onClose={() => setFilterVisible(false)}
|
||||
visible={filterVisible}
|
||||
open={filterVisible}
|
||||
width={400}
|
||||
>
|
||||
<Form layout="vertical">
|
||||
@@ -1209,7 +912,7 @@ export const ProductList: React.FC = () => {
|
||||
onChange={(value) => handleFilterChange('shop', value)}
|
||||
style={{ width: '100%' }}
|
||||
>
|
||||
{SHOPS.map(shop => (
|
||||
{shops.map(shop => (
|
||||
<Option key={shop.id} value={shop.id}>
|
||||
{shop.name} ({shop.platform})
|
||||
</Option>
|
||||
@@ -1362,7 +1065,7 @@ export const ProductList: React.FC = () => {
|
||||
title="排序设置"
|
||||
placement="right"
|
||||
onClose={() => setSortDrawerVisible(false)}
|
||||
visible={sortDrawerVisible}
|
||||
open={sortDrawerVisible}
|
||||
width={300}
|
||||
>
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
@@ -1403,7 +1106,7 @@ export const ProductList: React.FC = () => {
|
||||
title="列设置"
|
||||
placement="right"
|
||||
onClose={() => setColumnDrawerVisible(false)}
|
||||
visible={columnDrawerVisible}
|
||||
open={columnDrawerVisible}
|
||||
width={400}
|
||||
>
|
||||
<div style={{ marginBottom: 20 }}>
|
||||
@@ -1509,7 +1212,7 @@ export const ProductList: React.FC = () => {
|
||||
|
||||
<Modal
|
||||
title="商品定价"
|
||||
visible={pricingModalVisible}
|
||||
open={pricingModalVisible}
|
||||
onCancel={() => setPricingModalVisible(false)}
|
||||
footer={null}
|
||||
width={600}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
useState,
|
||||
Card,
|
||||
Form,
|
||||
Input,
|
||||
@@ -22,8 +22,6 @@ import {
|
||||
Image,
|
||||
Badge,
|
||||
Progress,
|
||||
} from 'antd';
|
||||
import {
|
||||
PlusOutlined,
|
||||
DeleteOutlined,
|
||||
SaveOutlined,
|
||||
@@ -37,7 +35,10 @@ import {
|
||||
TranslationOutlined,
|
||||
ThunderboltOutlined,
|
||||
EyeOutlined,
|
||||
} from '@ant-design/icons';
|
||||
Option,
|
||||
TextArea,
|
||||
FC,
|
||||
} from '@/imports';
|
||||
import type { UploadFile } from 'antd/es/upload/interface';
|
||||
import {
|
||||
materialProcessingDataSource,
|
||||
@@ -46,9 +47,7 @@ import {
|
||||
STYLE_OPTIONS,
|
||||
TRANSLATION_LANGUAGES,
|
||||
} from '@/services/materialProcessingDataSource';
|
||||
|
||||
const { Option } = Select;
|
||||
const { TextArea } = Input;
|
||||
import { useLocale } from '@/contexts/LocaleContext';
|
||||
|
||||
interface ProductVariant {
|
||||
id: string;
|
||||
@@ -98,7 +97,7 @@ const CURRENCIES = [
|
||||
{ value: 'CNY', label: 'CNY - Chinese Yuan' },
|
||||
];
|
||||
|
||||
export const ProductPublishForm: React.FC = () => {
|
||||
const ProductPublishForm: FC = () => {
|
||||
const [form] = Form.useForm();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [variants, setVariants] = useState<ProductVariant[]>([]);
|
||||
@@ -109,6 +108,7 @@ export const ProductPublishForm: React.FC = () => {
|
||||
const [selectedImage, setSelectedImage] = useState<string>('');
|
||||
const [processingType, setProcessingType] = useState<'image-to-image' | 'image-to-video'>('image-to-image');
|
||||
const [processingLoading, setProcessingLoading] = useState(false);
|
||||
const { t } = useLocale();
|
||||
|
||||
const handleAddVariant = () => {
|
||||
const newVariant: ProductVariant = {
|
||||
@@ -140,7 +140,7 @@ export const ProductPublishForm: React.FC = () => {
|
||||
|
||||
const handleProcessImage = async (params: ImageToImageParams | ImageToVideoParams) => {
|
||||
if (!selectedImage) {
|
||||
message.warning('请先选择要处理的图片');
|
||||
message.warning(t('product.publish.selectImage'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -154,7 +154,7 @@ export const ProductPublishForm: React.FC = () => {
|
||||
}
|
||||
|
||||
setProcessingJobs([...processingJobs, job]);
|
||||
message.success(`任务已创建: ${job.id}`);
|
||||
message.success(t('product.publish.taskCreated', { id: job.id }));
|
||||
setMaterialModalVisible(false);
|
||||
|
||||
// 轮询检查任务状态
|
||||
@@ -162,7 +162,7 @@ export const ProductPublishForm: React.FC = () => {
|
||||
const updatedJob = await materialProcessingDataSource.getJobStatus(job.id);
|
||||
if (updatedJob.status === 'completed') {
|
||||
clearInterval(checkInterval);
|
||||
message.success('素材处理完成!');
|
||||
message.success(t('product.publish.processingComplete'));
|
||||
// 将处理结果添加到文件列表
|
||||
if (processingType === 'image-to-image') {
|
||||
setFileList([...fileList, {
|
||||
@@ -173,13 +173,13 @@ export const ProductPublishForm: React.FC = () => {
|
||||
thumbUrl: updatedJob.outputUrl,
|
||||
} as UploadFile]);
|
||||
} else {
|
||||
message.info('视频已生成,可在素材库中查看');
|
||||
message.info(t('product.publish.videoGenerated'));
|
||||
}
|
||||
setProcessingJobs(processingJobs.map(j => j.id === job.id ? updatedJob : j));
|
||||
}
|
||||
}, 2000);
|
||||
} catch (error) {
|
||||
message.error('处理失败');
|
||||
message.error(t('product.publish.processFailed'));
|
||||
} finally {
|
||||
setProcessingLoading(false);
|
||||
}
|
||||
@@ -190,9 +190,9 @@ export const ProductPublishForm: React.FC = () => {
|
||||
await form.validateFields();
|
||||
setLoading(true);
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
message.success('Draft saved successfully');
|
||||
message.success(t('product.publish.draftSaved'));
|
||||
} catch (error) {
|
||||
message.error('Please fill in required fields');
|
||||
message.error(t('product.publish.fillRequiredFields'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -389,17 +389,17 @@ export const ProductPublishForm: React.FC = () => {
|
||||
<TextArea rows={4} placeholder="Enter product description" maxLength={5000} showCount />
|
||||
</Form.Item>
|
||||
|
||||
<Divider>Product Images</Divider>
|
||||
<Divider>{t('product.publish.productImages')}</Divider>
|
||||
|
||||
<Alert
|
||||
message="AI素材处理"
|
||||
description="上传图片后,可使用AI进行图生图、图生视频等处理。点击已上传图片上的AI按钮开始处理。"
|
||||
message={t('product.publish.materialProcessing')}
|
||||
description={t('product.publish.materialProcessingDesc')}
|
||||
type="info"
|
||||
showIcon
|
||||
style={{ marginBottom: 16 }}
|
||||
/>
|
||||
|
||||
<Form.Item label="Images">
|
||||
<Form.Item label={t('product.publish.images')}>
|
||||
<Upload
|
||||
listType="picture-card"
|
||||
fileList={fileList}
|
||||
@@ -421,7 +421,7 @@ export const ProductPublishForm: React.FC = () => {
|
||||
gap: 4,
|
||||
padding: '2px 0',
|
||||
}}>
|
||||
<Tooltip title="图生图">
|
||||
<Tooltip title={t('product.publish.imageToImage')}>
|
||||
<Button
|
||||
size="small"
|
||||
type="text"
|
||||
@@ -429,7 +429,7 @@ export const ProductPublishForm: React.FC = () => {
|
||||
onClick={() => handleOpenMaterialProcessing(file.url || file.thumbUrl || '', 'image-to-image')}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title="图生视频">
|
||||
<Tooltip title={t('product.publish.imageToVideo')}>
|
||||
<Button
|
||||
size="small"
|
||||
type="text"
|
||||
@@ -445,18 +445,18 @@ export const ProductPublishForm: React.FC = () => {
|
||||
{fileList.length >= 8 ? null : (
|
||||
<div>
|
||||
<PlusOutlined />
|
||||
<div style={{ marginTop: 8 }}>Upload</div>
|
||||
<div style={{ marginTop: 8 }}>{t('product.publish.upload')}</div>
|
||||
</div>
|
||||
)}
|
||||
</Upload>
|
||||
</Form.Item>
|
||||
|
||||
{processingJobs.length > 0 && (
|
||||
<Card size="small" title="处理中的任务" style={{ marginTop: 16 }}>
|
||||
<Card size="small" title={t('product.publish.processingTasks')} style={{ marginTop: 16 }}>
|
||||
{processingJobs.map(job => (
|
||||
<div key={job.id} style={{ marginBottom: 8 }}>
|
||||
<Space>
|
||||
<Tag color="blue">{job.type === 'image_to_image' ? '图生图' : '图生视频'}</Tag>
|
||||
<Tag color="blue">{job.type === 'image_to_image' ? t('product.publish.imageToImage') : t('product.publish.imageToVideo')}</Tag>
|
||||
<Progress percent={job.progress} size="small" style={{ width: 200 }} />
|
||||
<Tag color={job.status === 'processing' ? 'processing' : 'success'}>
|
||||
{job.status}
|
||||
@@ -468,11 +468,11 @@ export const ProductPublishForm: React.FC = () => {
|
||||
)}
|
||||
</>
|
||||
)},
|
||||
{ key: 'pricing', label: <><DollarOutlined /> Pricing</>, children: (
|
||||
{ key: 'pricing', label: <><DollarOutlined /> {t('product.publish.pricing')}</>, children: (
|
||||
<>
|
||||
<Alert
|
||||
message="Profit Margin Validation"
|
||||
description="B2B products must maintain at least 15% profit margin. B2C products require 20% minimum."
|
||||
message={t('product.publish.profitMarginValidation')}
|
||||
description={t('product.publish.profitMarginDesc')}
|
||||
type="info"
|
||||
showIcon
|
||||
style={{ marginBottom: 24 }}
|
||||
@@ -482,8 +482,8 @@ export const ProductPublishForm: React.FC = () => {
|
||||
<Col span={8}>
|
||||
<Form.Item
|
||||
name="price"
|
||||
label="Selling Price"
|
||||
rules={[{ required: true, message: 'Please enter price' }]}
|
||||
label={t('product.publish.sellingPrice')}
|
||||
rules={[{ required: true, message: t('product.publish.enterPrice') }]}
|
||||
>
|
||||
<InputNumber
|
||||
prefix="$"
|
||||
@@ -497,8 +497,8 @@ export const ProductPublishForm: React.FC = () => {
|
||||
<Col span={8}>
|
||||
<Form.Item
|
||||
name="costPrice"
|
||||
label="Cost Price"
|
||||
rules={[{ required: true, message: 'Please enter cost price' }]}
|
||||
label={t('product.publish.costPrice')}
|
||||
rules={[{ required: true, message: t('product.publish.enterCostPrice') }]}
|
||||
>
|
||||
<InputNumber
|
||||
prefix="$"
|
||||
@@ -510,7 +510,7 @@ export const ProductPublishForm: React.FC = () => {
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Form.Item name="currency" label="Currency">
|
||||
<Form.Item name="currency" label={t('product.publish.currency')}>
|
||||
<Select>
|
||||
{CURRENCIES.map(c => (
|
||||
<Option key={c.value} value={c.value}>{c.label}</Option>
|
||||
@@ -632,17 +632,17 @@ export const ProductPublishForm: React.FC = () => {
|
||||
title={
|
||||
<Space>
|
||||
<ThunderboltOutlined style={{ color: '#1890ff' }} />
|
||||
{processingType === 'image-to-image' ? '图生图' : '图生视频'}
|
||||
{processingType === 'image-to-image' ? t('product.publish.imageToImage') : t('product.publish.imageToVideo')}
|
||||
</Space>
|
||||
}
|
||||
visible={materialModalVisible}
|
||||
open={materialModalVisible}
|
||||
onCancel={() => setMaterialModalVisible(false)}
|
||||
footer={null}
|
||||
width={600}
|
||||
>
|
||||
{selectedImage && (
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<Text type="secondary">原图预览:</Text>
|
||||
<Text type="secondary">{t('product.publish.originalPreview')}</Text>
|
||||
<Image src={selectedImage} width={200} style={{ marginTop: 8, borderRadius: 8 }} />
|
||||
</div>
|
||||
)}
|
||||
@@ -650,15 +650,15 @@ export const ProductPublishForm: React.FC = () => {
|
||||
<Form layout="vertical" onFinish={handleProcessImage}>
|
||||
{processingType === 'image-to-image' ? (
|
||||
<>
|
||||
<Form.Item name="prompt" label="提示词">
|
||||
<Form.Item name="prompt" label={t('product.publish.prompt')}>
|
||||
<Input.TextArea
|
||||
rows={2}
|
||||
placeholder="描述你想要生成的图片效果..."
|
||||
placeholder={t('product.publish.promptPlaceholder')}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Form.Item name="style" label="风格" initialValue="realistic">
|
||||
<Form.Item name="style" label={t('product.publish.style')} initialValue="realistic">
|
||||
<Select>
|
||||
{STYLE_OPTIONS.map(opt => (
|
||||
<Option key={opt.value} value={opt.value}>{opt.label}</Option>
|
||||
@@ -667,7 +667,7 @@ export const ProductPublishForm: React.FC = () => {
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item name="strength" label="转换强度" initialValue={0.7}>
|
||||
<Form.Item name="strength" label={t('product.publish.strength')} initialValue={0.7}>
|
||||
<InputNumber min={0} max={1} step={0.1} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
@@ -675,30 +675,30 @@ export const ProductPublishForm: React.FC = () => {
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Form.Item name="prompt" label="提示词">
|
||||
<Form.Item name="prompt" label={t('product.publish.prompt')}>
|
||||
<Input.TextArea
|
||||
rows={2}
|
||||
placeholder="描述视频的运动效果..."
|
||||
placeholder={t('product.publish.promptPlaceholder')}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Row gutter={16}>
|
||||
<Col span={8}>
|
||||
<Form.Item name="motion" label="运动类型" initialValue="zoom">
|
||||
<Form.Item name="motion" label={t('product.publish.motion')} initialValue="zoom">
|
||||
<Select>
|
||||
<Option value="zoom">缩放</Option>
|
||||
<Option value="pan">平移</Option>
|
||||
<Option value="rotate">旋转</Option>
|
||||
<Option value="dynamic">动态</Option>
|
||||
<Option value="zoom">{t('product.publish.motionZoom')}</Option>
|
||||
<Option value="pan">{t('product.publish.motionPan')}</Option>
|
||||
<Option value="rotate">{t('product.publish.motionRotate')}</Option>
|
||||
<Option value="dynamic">{t('product.publish.motionDynamic')}</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Form.Item name="duration" label="时长(秒)" initialValue={5}>
|
||||
<Form.Item name="duration" label={t('product.publish.duration')} initialValue={5}>
|
||||
<InputNumber min={1} max={30} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Form.Item name="fps" label="帧率" initialValue={30}>
|
||||
<Form.Item name="fps" label={t('product.publish.fps')} initialValue={30}>
|
||||
<Select>
|
||||
<Option value={24}>24 FPS</Option>
|
||||
<Option value={30}>30 FPS</Option>
|
||||
@@ -713,10 +713,10 @@ export const ProductPublishForm: React.FC = () => {
|
||||
<Form.Item>
|
||||
<Space>
|
||||
<Button type="primary" htmlType="submit" loading={processingLoading}>
|
||||
开始处理
|
||||
{t('product.publish.startProcessing')}
|
||||
</Button>
|
||||
<Button onClick={() => setMaterialModalVisible(false)}>
|
||||
取消
|
||||
{t('product.publish.cancel')}
|
||||
</Button>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
|
||||
@@ -1,12 +1,40 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Card, Typography, Row, Col, Select, Button, Table, Statistic, Spin, message, Alert, Badge } from 'antd';
|
||||
import { LineChart, Line, AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts';
|
||||
import { AlertOutlined, UpOutlined, DownOutlined, ReloadOutlined, EyeOutlined } from '@ant-design/icons';
|
||||
//
|
||||
import { productDataSource, ProfitMonitor as ProfitMonitorData } from '@/services/productDataSource';
|
||||
|
||||
const { Title, Text, Paragraph } = Typography;
|
||||
const { Option } = Select;
|
||||
import {
|
||||
useState,
|
||||
useEffect,
|
||||
Card,
|
||||
Typography,
|
||||
Row,
|
||||
Col,
|
||||
Select,
|
||||
Button,
|
||||
Table,
|
||||
Statistic,
|
||||
Spin,
|
||||
message,
|
||||
Alert,
|
||||
Badge,
|
||||
LineChart,
|
||||
Line,
|
||||
AreaChart,
|
||||
Area,
|
||||
XAxis,
|
||||
YAxis,
|
||||
CartesianGrid,
|
||||
Tooltip,
|
||||
Legend,
|
||||
ResponsiveContainer,
|
||||
AlertOutlined,
|
||||
UpOutlined,
|
||||
DownOutlined,
|
||||
ReloadOutlined,
|
||||
EyeOutlined,
|
||||
Title,
|
||||
Text,
|
||||
Paragraph,
|
||||
Option,
|
||||
FC,
|
||||
} from '@/imports';
|
||||
import { productDataSource } from '@/services/productDataSource';
|
||||
|
||||
interface ProfitData {
|
||||
id: string;
|
||||
@@ -38,7 +66,7 @@ interface ProductProfit {
|
||||
status: 'up' | 'down' | 'stable';
|
||||
}
|
||||
|
||||
const ProfitMonitor: React.FC = () => {
|
||||
const ProfitMonitor: FC = () => {
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [profitData, setProfitData] = useState<ProfitData[]>([]);
|
||||
const [alerts, setAlerts] = useState<AlertItem[]>([]);
|
||||
|
||||
@@ -1,27 +1,50 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Card, Typography, Row, Col, DatePicker, Select, Button, Table, Statistic, Spin, message } from 'antd';
|
||||
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, LineChart, Line } from 'recharts';
|
||||
import { ArrowUpOutlined, ArrowDownOutlined, ReloadOutlined } from '@ant-design/icons';
|
||||
//
|
||||
import { productDataSource, ROIAnalysis as ROIAnalysisData } from '@/services/productDataSource';
|
||||
import {
|
||||
useState,
|
||||
useEffect,
|
||||
Card,
|
||||
Typography,
|
||||
Row,
|
||||
Col,
|
||||
DatePicker,
|
||||
Select,
|
||||
Button,
|
||||
Table,
|
||||
Statistic,
|
||||
Spin,
|
||||
message,
|
||||
BarChart,
|
||||
Bar,
|
||||
XAxis,
|
||||
YAxis,
|
||||
CartesianGrid,
|
||||
Tooltip,
|
||||
Legend,
|
||||
ResponsiveContainer,
|
||||
LineChart,
|
||||
Line,
|
||||
ArrowUpOutlined,
|
||||
ArrowDownOutlined,
|
||||
ReloadOutlined,
|
||||
Title,
|
||||
Text,
|
||||
RangePicker,
|
||||
Option,
|
||||
FC,
|
||||
} from '@/imports';
|
||||
import { productDataSource } from '@/services/productDataSource';
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
const { RangePicker } = DatePicker;
|
||||
const { Option } = Select;
|
||||
|
||||
interface ROIItem {
|
||||
id: string;
|
||||
productId: string;
|
||||
productName: string;
|
||||
cost: number;
|
||||
price: number;
|
||||
profit: number;
|
||||
roi: number;
|
||||
salesVolume: number;
|
||||
date: string;
|
||||
}
|
||||
|
||||
const ROIAnalysis: React.FC = () => {
|
||||
const ROIAnalysis: FC = () => {
|
||||
interface ROIItem {
|
||||
id: string;
|
||||
productId: string;
|
||||
productName: string;
|
||||
cost: number;
|
||||
price: number;
|
||||
profit: number;
|
||||
roi: number;
|
||||
salesVolume: number;
|
||||
date: string;
|
||||
}
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [roiData, setRoiData] = useState<ROIItem[]>([]);
|
||||
const [timeRange, setTimeRange] = useState<any>(null);
|
||||
|
||||
@@ -1,11 +1,38 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Card, Typography, Row, Col, Button, Table, Input, Select, Modal, Form, InputNumber, message, Tag, Space, Popconfirm, Tabs } from 'antd';
|
||||
import { PlusOutlined, UploadOutlined, EditOutlined, SearchOutlined, FilterOutlined, SyncOutlined, DeleteOutlined, EyeOutlined, GlobalOutlined } from '@ant-design/icons';
|
||||
import { Link } from 'react-router-dom';
|
||||
import {
|
||||
useState,
|
||||
useEffect,
|
||||
Card,
|
||||
Typography,
|
||||
Row,
|
||||
Col,
|
||||
Button,
|
||||
Table,
|
||||
Input,
|
||||
Select,
|
||||
Modal,
|
||||
Form,
|
||||
InputNumber,
|
||||
message,
|
||||
Tag,
|
||||
Space,
|
||||
Popconfirm,
|
||||
Tabs,
|
||||
PlusOutlined,
|
||||
UploadOutlined,
|
||||
EditOutlined,
|
||||
SearchOutlined,
|
||||
FilterOutlined,
|
||||
SyncOutlined,
|
||||
DeleteOutlined,
|
||||
EyeOutlined,
|
||||
GlobalOutlined,
|
||||
Link,
|
||||
Title,
|
||||
Text,
|
||||
FC,
|
||||
} from '@/imports';
|
||||
import CrossPlatformManage from './CrossPlatformManage';
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
|
||||
interface Product {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -18,7 +45,7 @@ interface Product {
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
const ProductManagement: React.FC = () => {
|
||||
const ProductManagement: FC = () => {
|
||||
const [products, setProducts] = useState<Product[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
|
||||
@@ -450,7 +477,7 @@ const ProductManagement: React.FC = () => {
|
||||
|
||||
<Modal
|
||||
title={editingProduct ? '编辑商品' : '新增商品'}
|
||||
visible={isModalVisible}
|
||||
open={isModalVisible}
|
||||
onOk={handleModalOk}
|
||||
onCancel={() => setIsModalVisible(false)}
|
||||
width={600}
|
||||
@@ -521,7 +548,7 @@ const ProductManagement: React.FC = () => {
|
||||
|
||||
<Modal
|
||||
title="商品详情"
|
||||
visible={isDetailModalVisible}
|
||||
open={isDetailModalVisible}
|
||||
onCancel={() => setIsDetailModalVisible(false)}
|
||||
footer={[
|
||||
<Button key="close" onClick={() => setIsDetailModalVisible(false)}>
|
||||
@@ -580,7 +607,7 @@ const ProductManagement: React.FC = () => {
|
||||
|
||||
<Modal
|
||||
title="商品定价"
|
||||
visible={isPricingModalVisible}
|
||||
open={isPricingModalVisible}
|
||||
onOk={handlePricingModalOk}
|
||||
onCancel={() => setIsPricingModalVisible(false)}
|
||||
width={600}
|
||||
|
||||
Reference in New Issue
Block a user