refactor(services): 重构服务文件结构,将服务按功能分类到不同目录

- 将服务文件按功能分类到core、ai、analytics、security等目录
- 修复logger导入路径问题,统一使用相对路径
- 更新相关文件的导入路径引用
- 添加新的批量操作组件导出文件
- 修复dashboard页面中的类型错误
- 添加dotenv依赖到package.json
This commit is contained in:
2026-03-25 13:46:26 +08:00
parent e59d7c6620
commit 2748456d8a
598 changed files with 74404 additions and 9576 deletions

View File

@@ -273,7 +273,7 @@ export const MaterialUpload: React.FC = () => {
dataIndex: 'tags',
key: 'tags',
width: 150,
render: (tags: string[]) => tags.map(t => <Tag key={t}>{t}</Tag>),
render: (tags: string[]) => tags?.map(t => <Tag key={t}>{t}</Tag>) || '-',
},
{
title: 'Actions',

View File

@@ -75,14 +75,27 @@ interface Product {
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;
@@ -136,6 +149,58 @@ const PLATFORMS = [
];
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' },
@@ -204,6 +269,9 @@ const MOCK_PRODUCTS: Product[] = [
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',
},
@@ -220,6 +288,9 @@ const MOCK_PRODUCTS: Product[] = [
stock: 128,
status: 'DRAFT',
platformStatus: {},
shopId: 'shop-tiktok-1',
shopName: 'TikTok旗舰店',
platform: 'TikTok',
createdAt: '2026-03-10',
updatedAt: '2026-03-10',
},
@@ -236,6 +307,9 @@ const MOCK_PRODUCTS: Product[] = [
stock: 64,
status: 'PRICED',
platformStatus: {},
shopId: 'shop-shopee-1',
shopName: 'Shopee官方店',
platform: 'Shopee',
createdAt: '2026-03-15',
updatedAt: '2026-03-16',
},
@@ -252,6 +326,9 @@ const MOCK_PRODUCTS: Product[] = [
stock: 512,
status: 'LISTED',
platformStatus: { Amazon: 'LISTED' },
shopId: 'shop-amazon-2',
shopName: 'Amazon欧洲店',
platform: 'Amazon',
createdAt: '2026-03-01',
updatedAt: '2026-03-17',
},
@@ -268,6 +345,9 @@ const MOCK_PRODUCTS: Product[] = [
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',
},
@@ -284,6 +364,9 @@ const MOCK_PRODUCTS: Product[] = [
stock: 0,
status: 'SYNC_FAILED',
platformStatus: { Amazon: 'FAILED' },
shopId: 'shop-temu-1',
shopName: 'Temu旗舰店',
platform: 'Temu',
createdAt: '2026-03-05',
updatedAt: '2026-03-18',
},
@@ -300,6 +383,9 @@ const MOCK_PRODUCTS: Product[] = [
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',
},
@@ -322,6 +408,7 @@ export const ProductList: React.FC = () => {
keyword: '',
status: [],
platform: [],
shop: [],
category: [],
priceRange: null,
stockRange: null,
@@ -388,6 +475,7 @@ export const ProductList: React.FC = () => {
keyword: '',
status: [],
platform: [],
shop: [],
category: [],
priceRange: null,
stockRange: null,
@@ -606,6 +694,9 @@ export const ProductList: React.FC = () => {
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) {
@@ -1110,6 +1201,21 @@ export const ProductList: React.FC = () => {
))}
</Select>
</Form.Item>
<Form.Item label="店铺">
<Select
mode="multiple"
placeholder="选择店铺"
value={filters.shop}
onChange={(value) => handleFilterChange('shop', value)}
style={{ width: '100%' }}
>
{SHOPS.map(shop => (
<Option key={shop.id} value={shop.id}>
{shop.name} ({shop.platform})
</Option>
))}
</Select>
</Form.Item>
<Form.Item label="平台">
<Select
mode="multiple"

View File

@@ -19,6 +19,9 @@ import {
Switch,
Tooltip,
Alert,
Image,
Badge,
Progress,
} from 'antd';
import {
PlusOutlined,
@@ -29,8 +32,20 @@ import {
ShopOutlined,
DollarOutlined,
BoxPlotOutlined,
PictureOutlined,
VideoCameraOutlined,
TranslationOutlined,
ThunderboltOutlined,
EyeOutlined,
} from '@ant-design/icons';
import type { UploadFile } from 'antd/es/upload/interface';
import {
materialProcessingDataSource,
ImageToImageParams,
ImageToVideoParams,
STYLE_OPTIONS,
TRANSLATION_LANGUAGES,
} from '@/services/materialProcessingDataSource';
const { Option } = Select;
const { TextArea } = Input;
@@ -89,6 +104,11 @@ export const ProductPublishForm: React.FC = () => {
const [variants, setVariants] = useState<ProductVariant[]>([]);
const [fileList, setFileList] = useState<UploadFile[]>([]);
const [activeTab, setActiveTab] = useState('basic');
const [processingJobs, setProcessingJobs] = useState<any[]>([]);
const [materialModalVisible, setMaterialModalVisible] = useState(false);
const [selectedImage, setSelectedImage] = useState<string>('');
const [processingType, setProcessingType] = useState<'image-to-image' | 'image-to-video'>('image-to-image');
const [processingLoading, setProcessingLoading] = useState(false);
const handleAddVariant = () => {
const newVariant: ProductVariant = {
@@ -112,6 +132,59 @@ export const ProductPublishForm: React.FC = () => {
));
};
const handleOpenMaterialProcessing = (imageUrl: string, type: 'image-to-image' | 'image-to-video') => {
setSelectedImage(imageUrl);
setProcessingType(type);
setMaterialModalVisible(true);
};
const handleProcessImage = async (params: ImageToImageParams | ImageToVideoParams) => {
if (!selectedImage) {
message.warning('请先选择要处理的图片');
return;
}
setProcessingLoading(true);
try {
let job;
if (processingType === 'image-to-image') {
job = await materialProcessingDataSource.imageToImage(selectedImage, params as ImageToImageParams);
} else {
job = await materialProcessingDataSource.imageToVideo(selectedImage, params as ImageToVideoParams);
}
setProcessingJobs([...processingJobs, job]);
message.success(`任务已创建: ${job.id}`);
setMaterialModalVisible(false);
// 轮询检查任务状态
const checkInterval = setInterval(async () => {
const updatedJob = await materialProcessingDataSource.getJobStatus(job.id);
if (updatedJob.status === 'completed') {
clearInterval(checkInterval);
message.success('素材处理完成!');
// 将处理结果添加到文件列表
if (processingType === 'image-to-image') {
setFileList([...fileList, {
uid: `processed-${Date.now()}`,
name: `processed-${Date.now()}.jpg`,
status: 'done',
url: updatedJob.outputUrl,
thumbUrl: updatedJob.outputUrl,
} as UploadFile]);
} else {
message.info('视频已生成,可在素材库中查看');
}
setProcessingJobs(processingJobs.map(j => j.id === job.id ? updatedJob : j));
}
}, 2000);
} catch (error) {
message.error('处理失败');
} finally {
setProcessingLoading(false);
}
};
const handleSaveDraft = async () => {
try {
await form.validateFields();
@@ -318,6 +391,14 @@ export const ProductPublishForm: React.FC = () => {
<Divider>Product Images</Divider>
<Alert
message="AI素材处理"
description="上传图片后可使用AI进行图生图、图生视频等处理。点击已上传图片上的AI按钮开始处理。"
type="info"
showIcon
style={{ marginBottom: 16 }}
/>
<Form.Item label="Images">
<Upload
listType="picture-card"
@@ -325,6 +406,41 @@ export const ProductPublishForm: React.FC = () => {
onChange={({ fileList }) => setFileList(fileList)}
beforeUpload={() => false}
multiple
itemRender={(originNode, file) => (
<div style={{ position: 'relative' }}>
{originNode}
{file.status === 'done' && file.url && (
<div style={{
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
background: 'rgba(0,0,0,0.5)',
display: 'flex',
justifyContent: 'center',
gap: 4,
padding: '2px 0',
}}>
<Tooltip title="图生图">
<Button
size="small"
type="text"
icon={<PictureOutlined style={{ color: '#fff' }} />}
onClick={() => handleOpenMaterialProcessing(file.url || file.thumbUrl || '', 'image-to-image')}
/>
</Tooltip>
<Tooltip title="图生视频">
<Button
size="small"
type="text"
icon={<VideoCameraOutlined style={{ color: '#fff' }} />}
onClick={() => handleOpenMaterialProcessing(file.url || file.thumbUrl || '', 'image-to-video')}
/>
</Tooltip>
</div>
)}
</div>
)}
>
{fileList.length >= 8 ? null : (
<div>
@@ -334,6 +450,22 @@ export const ProductPublishForm: React.FC = () => {
)}
</Upload>
</Form.Item>
{processingJobs.length > 0 && (
<Card size="small" title="处理中的任务" style={{ marginTop: 16 }}>
{processingJobs.map(job => (
<div key={job.id} style={{ marginBottom: 8 }}>
<Space>
<Tag color="blue">{job.type === 'image_to_image' ? '图生图' : '图生视频'}</Tag>
<Progress percent={job.progress} size="small" style={{ width: 200 }} />
<Tag color={job.status === 'processing' ? 'processing' : 'success'}>
{job.status}
</Tag>
</Space>
</div>
))}
</Card>
)}
</>
)},
{ key: 'pricing', label: <><DollarOutlined /> Pricing</>, children: (
@@ -495,6 +627,101 @@ export const ProductPublishForm: React.FC = () => {
</Button>
</Space>
</Form>
<Modal
title={
<Space>
<ThunderboltOutlined style={{ color: '#1890ff' }} />
{processingType === 'image-to-image' ? '图生图' : '图生视频'}
</Space>
}
visible={materialModalVisible}
onCancel={() => setMaterialModalVisible(false)}
footer={null}
width={600}
>
{selectedImage && (
<div style={{ marginBottom: 16 }}>
<Text type="secondary"></Text>
<Image src={selectedImage} width={200} style={{ marginTop: 8, borderRadius: 8 }} />
</div>
)}
<Form layout="vertical" onFinish={handleProcessImage}>
{processingType === 'image-to-image' ? (
<>
<Form.Item name="prompt" label="提示词">
<Input.TextArea
rows={2}
placeholder="描述你想要生成的图片效果..."
/>
</Form.Item>
<Row gutter={16}>
<Col span={12}>
<Form.Item name="style" label="风格" initialValue="realistic">
<Select>
{STYLE_OPTIONS.map(opt => (
<Option key={opt.value} value={opt.value}>{opt.label}</Option>
))}
</Select>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name="strength" label="转换强度" initialValue={0.7}>
<InputNumber min={0} max={1} step={0.1} style={{ width: '100%' }} />
</Form.Item>
</Col>
</Row>
</>
) : (
<>
<Form.Item name="prompt" label="提示词">
<Input.TextArea
rows={2}
placeholder="描述视频的运动效果..."
/>
</Form.Item>
<Row gutter={16}>
<Col span={8}>
<Form.Item name="motion" label="运动类型" initialValue="zoom">
<Select>
<Option value="zoom"></Option>
<Option value="pan"></Option>
<Option value="rotate"></Option>
<Option value="dynamic"></Option>
</Select>
</Form.Item>
</Col>
<Col span={8}>
<Form.Item name="duration" label="时长(秒)" initialValue={5}>
<InputNumber min={1} max={30} style={{ width: '100%' }} />
</Form.Item>
</Col>
<Col span={8}>
<Form.Item name="fps" label="帧率" initialValue={30}>
<Select>
<Option value={24}>24 FPS</Option>
<Option value={30}>30 FPS</Option>
<Option value={60}>60 FPS</Option>
</Select>
</Form.Item>
</Col>
</Row>
</>
)}
<Form.Item>
<Space>
<Button type="primary" htmlType="submit" loading={processingLoading}>
</Button>
<Button onClick={() => setMaterialModalVisible(false)}>
</Button>
</Space>
</Form.Item>
</Form>
</Modal>
</Card>
</div>
);