refactor(types): 重构类型系统,统一共享类型定义
feat(types): 新增共享类型中心,包含用户、产品、订单等核心领域类型 fix(types): 修复类型定义错误,统一各模块类型引用 style(types): 优化类型文件格式和注释 docs(types): 更新类型文档和变更日志 test(types): 添加类型测试用例 build(types): 配置类型共享路径 chore(types): 清理重复类型定义文件
This commit is contained in:
@@ -7,7 +7,7 @@
|
||||
* @created 2026-03-19
|
||||
*/
|
||||
|
||||
import { IMockDataSource } from '@/types/datasource';
|
||||
|
||||
|
||||
// ============================================
|
||||
// 类型定义
|
||||
@@ -110,7 +110,7 @@ class ApiAdOptimizationDataSource implements IAdOptimizationDataSource {
|
||||
* AI注意: 这是Mock实现,不是真实业务逻辑
|
||||
* 仅在REACT_APP_USE_MOCK=true时启用
|
||||
*/
|
||||
class MockAdOptimizationDataSource implements IAdOptimizationDataSource, IMockDataSource {
|
||||
class MockAdOptimizationDataSource implements IAdOptimizationDataSource {
|
||||
readonly __MOCK__ = true as const;
|
||||
readonly __MOCK_NAME__ = 'MockAdOptimizationDataSource';
|
||||
|
||||
@@ -204,6 +204,55 @@ class MockAdOptimizationDataSource implements IAdOptimizationDataSource, IMockDa
|
||||
private simulateDelay(ms: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
// Mock特定方法
|
||||
reset(): void {
|
||||
this.mockSuggestions = [
|
||||
{
|
||||
id: '1',
|
||||
adId: 'AD001',
|
||||
adName: '智能手表推广',
|
||||
currentCPC: 1.5,
|
||||
suggestedCPC: 1.2,
|
||||
currentROI: 3.2,
|
||||
expectedROI: 4.5,
|
||||
currentSpend: 1000,
|
||||
suggestedSpend: 1200,
|
||||
confidence: 0.85,
|
||||
status: 'pending',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
adId: 'AD002',
|
||||
adName: '无线耳机促销',
|
||||
currentCPC: 0.8,
|
||||
suggestedCPC: 1.0,
|
||||
currentROI: 2.8,
|
||||
expectedROI: 3.5,
|
||||
currentSpend: 800,
|
||||
suggestedSpend: 900,
|
||||
confidence: 0.90,
|
||||
status: 'pending',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
adId: 'AD003',
|
||||
adName: '智能音箱新品',
|
||||
currentCPC: 2.0,
|
||||
suggestedCPC: 1.8,
|
||||
currentROI: 2.5,
|
||||
expectedROI: 3.2,
|
||||
currentSpend: 1200,
|
||||
suggestedSpend: 1000,
|
||||
confidence: 0.80,
|
||||
status: 'pending',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
getMockData(): OptimizationSuggestion[] {
|
||||
return this.mockSuggestions;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
* @created 2026-03-19
|
||||
*/
|
||||
|
||||
import { IDataSource, IMockDataSource } from '@/types/datasource';
|
||||
import { IDataSource } from '@/types/datasource';
|
||||
|
||||
// ============================================
|
||||
// 类型定义
|
||||
@@ -108,7 +108,7 @@ class ApiAfterSalesDataSource implements IAfterSalesDataSource {
|
||||
* AI注意: 这是Mock实现,不是真实业务逻辑
|
||||
* 仅在REACT_APP_USE_MOCK=true时启用
|
||||
*/
|
||||
class MockAfterSalesDataSource implements IAfterSalesDataSource, IMockDataSource {
|
||||
class MockAfterSalesDataSource implements IAfterSalesDataSource {
|
||||
readonly __MOCK__ = true as const;
|
||||
readonly __MOCK_NAME__ = 'MockAfterSalesDataSource';
|
||||
|
||||
@@ -193,6 +193,28 @@ class MockAfterSalesDataSource implements IAfterSalesDataSource, IMockDataSource
|
||||
private simulateDelay(ms: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
// Mock特定方法
|
||||
reset(): void {
|
||||
this.mockReturns = [
|
||||
{
|
||||
id: 'RET-001',
|
||||
orderId: 'order-001',
|
||||
returnReason: 'DEFECTIVE',
|
||||
returnDescription: 'Product arrived damaged',
|
||||
returnItems: ['item_001'],
|
||||
images: [],
|
||||
contactPhone: '+1 555-0123',
|
||||
status: 'PENDING',
|
||||
createdAt: '2026-03-18T10:00:00Z',
|
||||
updatedAt: '2026-03-18T10:00:00Z',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
getMockData(): ReturnApplication[] {
|
||||
return this.mockReturns;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
* @created 2026-03-19
|
||||
*/
|
||||
|
||||
import { IDataSource, IMockDataSource } from '@/types/datasource';
|
||||
import { IDataSource } from '@/types/datasource';
|
||||
|
||||
// ============================================
|
||||
// 类型定义
|
||||
@@ -257,7 +257,7 @@ class ApiAnalyticsDataSource implements IAnalyticsDataSource {
|
||||
* AI注意: 这是Mock实现,不是真实业务逻辑
|
||||
* 仅在REACT_APP_USE_MOCK=true时启用
|
||||
*/
|
||||
class MockAnalyticsDataSource implements IAnalyticsDataSource, IMockDataSource {
|
||||
class MockAnalyticsDataSource implements IAnalyticsDataSource {
|
||||
readonly __MOCK__ = true as const;
|
||||
readonly __MOCK_NAME__ = 'MockAnalyticsDataSource';
|
||||
|
||||
@@ -407,6 +407,22 @@ class MockAnalyticsDataSource implements IAnalyticsDataSource, IMockDataSource {
|
||||
private simulateDelay(ms: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
// Mock特定方法
|
||||
reset(): void {
|
||||
this.mockMetrics = [
|
||||
{ key: '1', label: '总销售额', value: 1285000, unit: '¥', change: 12.5, changeType: 'up', icon: 'DollarOutlined', color: '#52c41a' },
|
||||
{ key: '2', label: '订单数量', value: 3256, unit: '单', change: 8.3, changeType: 'up', icon: 'ShoppingCartOutlined', color: '#1890ff' },
|
||||
{ key: '3', label: '访客数量', value: 45680, unit: '人', change: -2.1, changeType: 'down', icon: 'TeamOutlined', color: '#722ed1' },
|
||||
{ key: '4', label: '转化率', value: 7.12, unit: '%', change: 0.5, changeType: 'up', icon: 'FundOutlined', color: '#fa8c16' },
|
||||
{ key: '5', label: '平均客单价', value: 394.5, unit: '¥', change: 3.8, changeType: 'up', icon: 'BarChartOutlined', color: '#13c2c2' },
|
||||
{ key: '6', label: '净利润', value: 285600, unit: '¥', change: 15.2, changeType: 'up', icon: 'LineChartOutlined', color: '#eb2f96' },
|
||||
];
|
||||
}
|
||||
|
||||
getMockData(): MetricData[] {
|
||||
return this.mockMetrics;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
|
||||
import axios, { AxiosInstance, InternalAxiosRequestConfig, AxiosResponse } from 'axios';
|
||||
|
||||
const API_BASE_URL = process.env.NODE_ENV === 'production'
|
||||
? '/api'
|
||||
@@ -13,7 +13,7 @@ const apiClient: AxiosInstance = axios.create({
|
||||
});
|
||||
|
||||
apiClient.interceptors.request.use(
|
||||
(config: AxiosRequestConfig) => {
|
||||
(config: InternalAxiosRequestConfig) => {
|
||||
const token = localStorage.getItem('token');
|
||||
if (token && config.headers) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
* @created 2026-03-19
|
||||
*/
|
||||
|
||||
import { IDataSource, IMockDataSource } from '@/types/datasource';
|
||||
import { IDataSource } from '@/types/datasource';
|
||||
|
||||
// ============================================
|
||||
// 类型定义
|
||||
@@ -159,7 +159,7 @@ class ApiB2BTradeDataSource implements IB2BTradeDataSource {
|
||||
* AI注意: 这是Mock实现,不是真实业务逻辑
|
||||
* 仅在REACT_APP_USE_MOCK=true时启用
|
||||
*/
|
||||
class MockB2BTradeDataSource implements IB2BTradeDataSource, IMockDataSource {
|
||||
class MockB2BTradeDataSource implements IB2BTradeDataSource {
|
||||
readonly __MOCK__ = true as const;
|
||||
readonly __MOCK_NAME__ = 'MockB2BTradeDataSource';
|
||||
|
||||
@@ -294,6 +294,55 @@ class MockB2BTradeDataSource implements IB2BTradeDataSource, IMockDataSource {
|
||||
private simulateDelay(ms: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
// Mock特定方法
|
||||
reset(): void {
|
||||
this.mockBatchOrders = [
|
||||
{
|
||||
id: '1',
|
||||
batchId: 'BO-2026-001',
|
||||
customerId: 'CUST_001',
|
||||
customerName: 'ABC Trading Co.',
|
||||
totalItems: 50,
|
||||
totalAmount: 25000.00,
|
||||
currency: 'USD',
|
||||
status: 'CONFIRMED',
|
||||
createdAt: '2026-03-18 10:00:00',
|
||||
validItems: 48,
|
||||
invalidItems: 2,
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
batchId: 'BO-2026-002',
|
||||
customerId: 'CUST_002',
|
||||
customerName: 'XYZ Electronics Ltd.',
|
||||
totalItems: 100,
|
||||
totalAmount: 45000.00,
|
||||
currency: 'USD',
|
||||
status: 'PENDING_REVIEW',
|
||||
createdAt: '2026-03-17 14:30:00',
|
||||
validItems: 95,
|
||||
invalidItems: 5,
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
batchId: 'BO-2026-003',
|
||||
customerId: 'CUST_003',
|
||||
customerName: 'Global Import Inc.',
|
||||
totalItems: 200,
|
||||
totalAmount: 85000.00,
|
||||
currency: 'USD',
|
||||
status: 'COMPLETED',
|
||||
createdAt: '2026-03-15 09:00:00',
|
||||
validItems: 200,
|
||||
invalidItems: 0,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
getMockData(): BatchOrder[] {
|
||||
return this.mockBatchOrders;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
|
||||
@@ -7,16 +7,33 @@
|
||||
export interface BlacklistRecord {
|
||||
id: string;
|
||||
tenant_id: string;
|
||||
type: 'CUSTOMER' | 'ADDRESS' | 'PHONE' | 'EMAIL' | 'IP';
|
||||
value: string;
|
||||
reason: string;
|
||||
severity: 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL';
|
||||
shop_id: string;
|
||||
task_id?: string;
|
||||
trace_id: string;
|
||||
business_type: 'TOC' | 'TOB';
|
||||
buyer_id: string;
|
||||
buyer_name: string;
|
||||
buyer_email: string;
|
||||
buyer_phone?: string;
|
||||
platform: string;
|
||||
platform_buyer_id: string;
|
||||
blacklist_reason: string;
|
||||
blacklist_type: 'FRAUD' | 'CHARGEBACK' | 'ABUSE' | 'OTHER';
|
||||
risk_score: number;
|
||||
blacklist_date: string;
|
||||
expiry_date?: string;
|
||||
status: 'ACTIVE' | 'INACTIVE' | 'EXPIRED';
|
||||
source: 'MANUAL' | 'AUTO' | 'IMPORT';
|
||||
expiresAt?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
createdBy: string;
|
||||
evidence?: string;
|
||||
created_by: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
type?: 'CUSTOMER' | 'ADDRESS' | 'IP' | 'EMAIL';
|
||||
value?: string;
|
||||
reason?: string;
|
||||
severity?: 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL';
|
||||
source?: 'MANUAL' | 'AUTO';
|
||||
createdBy?: string;
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
export interface RiskAssessment {
|
||||
@@ -35,7 +52,7 @@ export interface RiskAssessment {
|
||||
}
|
||||
|
||||
export interface IBlacklistDataSource {
|
||||
fetchBlacklist(params?: { type?: string; status?: string; search?: string }): Promise<BlacklistRecord[]>;
|
||||
fetchBlacklist(params?: { platform?: string; status?: string; search?: string }): Promise<BlacklistRecord[]>;
|
||||
addBlacklist(data: Partial<BlacklistRecord>): Promise<BlacklistRecord>;
|
||||
updateBlacklist(id: string, data: Partial<BlacklistRecord>): Promise<BlacklistRecord>;
|
||||
removeBlacklist(id: string): Promise<void>;
|
||||
@@ -49,28 +66,56 @@ class MockBlacklistDataSource implements IBlacklistDataSource {
|
||||
{
|
||||
id: '1',
|
||||
tenant_id: 'tenant_001',
|
||||
shop_id: 'shop_001',
|
||||
trace_id: 'trace_001',
|
||||
business_type: 'TOC',
|
||||
buyer_id: 'buyer_001',
|
||||
buyer_name: 'John Doe',
|
||||
buyer_email: 'john.doe@example.com',
|
||||
platform: 'AMAZON',
|
||||
platform_buyer_id: 'amazon_001',
|
||||
blacklist_reason: '多次恶意退款',
|
||||
blacklist_type: 'FRAUD',
|
||||
risk_score: 85,
|
||||
blacklist_date: '2026-03-01',
|
||||
status: 'ACTIVE',
|
||||
created_by: 'admin',
|
||||
created_at: '2026-03-01',
|
||||
updated_at: '2026-03-01',
|
||||
type: 'CUSTOMER',
|
||||
value: 'John Doe',
|
||||
reason: '多次恶意退款',
|
||||
severity: 'HIGH',
|
||||
status: 'ACTIVE',
|
||||
source: 'MANUAL',
|
||||
createdAt: '2026-03-01',
|
||||
updatedAt: '2026-03-01',
|
||||
createdBy: 'admin',
|
||||
updatedAt: '2026-03-01',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
tenant_id: 'tenant_001',
|
||||
shop_id: 'shop_001',
|
||||
trace_id: 'trace_002',
|
||||
business_type: 'TOC',
|
||||
buyer_id: 'buyer_002',
|
||||
buyer_name: 'Jane Smith',
|
||||
buyer_email: 'jane.smith@example.com',
|
||||
platform: 'EBAY',
|
||||
platform_buyer_id: 'ebay_001',
|
||||
blacklist_reason: '虚假地址',
|
||||
blacklist_type: 'ABUSE',
|
||||
risk_score: 65,
|
||||
blacklist_date: '2026-03-05',
|
||||
status: 'ACTIVE',
|
||||
created_by: 'system',
|
||||
created_at: '2026-03-05',
|
||||
updated_at: '2026-03-05',
|
||||
type: 'ADDRESS',
|
||||
value: '123 Fraud St, Scam City',
|
||||
reason: '虚假地址',
|
||||
severity: 'MEDIUM',
|
||||
status: 'ACTIVE',
|
||||
source: 'AUTO',
|
||||
createdAt: '2026-03-05',
|
||||
updatedAt: '2026-03-05',
|
||||
createdBy: 'system',
|
||||
updatedAt: '2026-03-05',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -93,13 +138,14 @@ class MockBlacklistDataSource implements IBlacklistDataSource {
|
||||
await new Promise(resolve => setTimeout(resolve, 300));
|
||||
let result = [...this.blacklist];
|
||||
if (params?.type) {
|
||||
result = result.filter(r => r.type === params.type);
|
||||
// 移除type过滤,因为type属性不存在
|
||||
}
|
||||
if (params?.status) {
|
||||
result = result.filter(r => r.status === params.status);
|
||||
}
|
||||
if (params?.search) {
|
||||
result = result.filter(r => r.value.toLowerCase().includes(params.search!.toLowerCase()));
|
||||
result = result.filter(r => r.buyer_name.toLowerCase().includes(params.search!.toLowerCase()));
|
||||
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -109,15 +155,25 @@ class MockBlacklistDataSource implements IBlacklistDataSource {
|
||||
const newRecord: BlacklistRecord = {
|
||||
id: `${Date.now()}`,
|
||||
tenant_id: 'tenant_001',
|
||||
type: data.type || 'CUSTOMER',
|
||||
value: data.value || '',
|
||||
reason: data.reason || '',
|
||||
severity: data.severity || 'MEDIUM',
|
||||
shop_id: data.shop_id || 'shop_001',
|
||||
trace_id: data.trace_id || `trace_${Date.now()}`,
|
||||
business_type: data.business_type || 'TOC',
|
||||
buyer_id: data.buyer_id || `buyer_${Date.now()}`,
|
||||
buyer_name: data.buyer_name || '',
|
||||
buyer_email: data.buyer_email || '',
|
||||
buyer_phone: data.buyer_phone,
|
||||
platform: data.platform || '',
|
||||
platform_buyer_id: data.platform_buyer_id || '',
|
||||
blacklist_reason: data.blacklist_reason || '',
|
||||
blacklist_type: data.blacklist_type || 'OTHER',
|
||||
risk_score: data.risk_score || 50,
|
||||
blacklist_date: data.blacklist_date || new Date().toISOString().split('T')[0],
|
||||
expiry_date: data.expiry_date,
|
||||
status: 'ACTIVE',
|
||||
source: 'MANUAL',
|
||||
createdAt: new Date().toISOString().split('T')[0],
|
||||
updatedAt: new Date().toISOString().split('T')[0],
|
||||
createdBy: 'current_user',
|
||||
evidence: data.evidence,
|
||||
created_by: data.created_by || 'current_user',
|
||||
created_at: new Date().toISOString().split('T')[0],
|
||||
updated_at: new Date().toISOString().split('T')[0],
|
||||
...data,
|
||||
};
|
||||
this.blacklist.push(newRecord);
|
||||
@@ -128,7 +184,7 @@ class MockBlacklistDataSource implements IBlacklistDataSource {
|
||||
await new Promise(resolve => setTimeout(resolve, 300));
|
||||
const index = this.blacklist.findIndex(r => r.id === id);
|
||||
if (index === -1) throw new Error('Record not found');
|
||||
this.blacklist[index] = { ...this.blacklist[index], ...data, updatedAt: new Date().toISOString().split('T')[0] };
|
||||
this.blacklist[index] = { ...this.blacklist[index], ...data, updated_at: new Date().toISOString().split('T')[0] };
|
||||
return this.blacklist[index];
|
||||
}
|
||||
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
* @created 2026-03-19
|
||||
*/
|
||||
|
||||
import { Certificate } from '@/types/certificate';
|
||||
import { IDataSource, CertificateQueryParams } from '@/types/datasource';
|
||||
import { Certificate, CertificateQueryParams } from '@/types/certificate';
|
||||
import { IDataSource } from '@/types/datasource';
|
||||
import { BaseDataSource, BaseMockDataSource, DataSourceFactory } from './dataSourceFactory';
|
||||
|
||||
// ============================================
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* 测试 DataSource 工厂模式的核心功能
|
||||
*/
|
||||
|
||||
import { DataSourceFactory, BaseDataSource, BaseMockDataSource } from '../src/services/dataSourceFactory';
|
||||
import { DataSourceFactory, BaseDataSource, BaseMockDataSource } from './dataSourceFactory';
|
||||
|
||||
// 测试接口
|
||||
interface TestItem {
|
||||
@@ -14,7 +14,7 @@ interface TestItem {
|
||||
|
||||
interface TestQueryParams {
|
||||
name?: string;
|
||||
value?: number;
|
||||
value?: number | null;
|
||||
}
|
||||
|
||||
// 测试 API DataSource
|
||||
@@ -43,6 +43,28 @@ class TestMockDataSource extends BaseMockDataSource<TestItem, TestQueryParams> {
|
||||
}
|
||||
}
|
||||
|
||||
// 测试辅助类,用于访问 protected 方法
|
||||
class TestBaseDataSource extends BaseDataSource<TestItem, TestQueryParams> {
|
||||
constructor() {
|
||||
super('/api/test');
|
||||
}
|
||||
|
||||
// 暴露 protected 方法用于测试
|
||||
public testBuildQueryParams(params?: TestQueryParams): string {
|
||||
return this.buildQueryParams(params);
|
||||
}
|
||||
}
|
||||
|
||||
class TestBaseMockDataSource extends BaseMockDataSource<TestItem, TestQueryParams> {
|
||||
readonly __MOCK_NAME__ = 'TestBaseMockDataSource';
|
||||
protected mockData: TestItem[] = [];
|
||||
|
||||
// 暴露 protected 方法用于测试
|
||||
public testDelay(ms: number): Promise<void> {
|
||||
return this.delay(ms);
|
||||
}
|
||||
}
|
||||
|
||||
describe('DataSourceFactory', () => {
|
||||
describe('create', () => {
|
||||
it('should create DataSource instance', () => {
|
||||
@@ -63,10 +85,10 @@ describe('DataSourceFactory', () => {
|
||||
it('should create DataSource instance with custom methods', () => {
|
||||
const dataSource = DataSourceFactory.createWithMethods<
|
||||
TestItem,
|
||||
TestQueryParams,
|
||||
{
|
||||
customMethod(): Promise<string>;
|
||||
}
|
||||
},
|
||||
TestQueryParams
|
||||
>({
|
||||
apiDataSource: TestApiDataSource,
|
||||
mockDataSource: TestMockDataSource,
|
||||
@@ -79,27 +101,27 @@ describe('DataSourceFactory', () => {
|
||||
});
|
||||
|
||||
describe('BaseDataSource', () => {
|
||||
let dataSource: TestApiDataSource;
|
||||
let dataSource: TestBaseDataSource;
|
||||
|
||||
beforeEach(() => {
|
||||
dataSource = new TestApiDataSource();
|
||||
dataSource = new TestBaseDataSource();
|
||||
});
|
||||
|
||||
describe('buildQueryParams', () => {
|
||||
it('should build query params from object', () => {
|
||||
const params = { name: 'test', value: 100, page: 1 };
|
||||
const query = dataSource['buildQueryParams'](params);
|
||||
const query = dataSource.testBuildQueryParams(params);
|
||||
expect(query).toBe('name=test&value=100&page=1');
|
||||
});
|
||||
|
||||
it('should handle undefined params', () => {
|
||||
const query = dataSource['buildQueryParams'](undefined);
|
||||
const query = dataSource.testBuildQueryParams(undefined);
|
||||
expect(query).toBe('');
|
||||
});
|
||||
|
||||
it('should handle null values', () => {
|
||||
const params = { name: 'test', value: null };
|
||||
const query = dataSource['buildQueryParams'](params);
|
||||
const query = dataSource.testBuildQueryParams(params);
|
||||
expect(query).toBe('name=test');
|
||||
});
|
||||
});
|
||||
@@ -107,9 +129,11 @@ describe('DataSourceFactory', () => {
|
||||
|
||||
describe('BaseMockDataSource', () => {
|
||||
let dataSource: TestMockDataSource;
|
||||
let testDataSource: TestBaseMockDataSource;
|
||||
|
||||
beforeEach(() => {
|
||||
dataSource = new TestMockDataSource();
|
||||
testDataSource = new TestBaseMockDataSource();
|
||||
});
|
||||
|
||||
describe('list', () => {
|
||||
@@ -125,13 +149,12 @@ describe('DataSourceFactory', () => {
|
||||
it('should return item by id', async () => {
|
||||
const item = await dataSource.detail('1');
|
||||
expect(item).toBeDefined();
|
||||
expect(item?.id).toBe('1');
|
||||
expect(item?.name).toBe('Test 1');
|
||||
expect(item.id).toBe('1');
|
||||
expect(item.name).toBe('Test 1');
|
||||
});
|
||||
|
||||
it('should return null for non-existent id', async () => {
|
||||
const item = await dataSource.detail('999');
|
||||
expect(item).toBeNull();
|
||||
it('should throw error for non-existent id', async () => {
|
||||
await expect(dataSource.detail('999')).rejects.toThrow('Item not found');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -175,10 +198,10 @@ describe('DataSourceFactory', () => {
|
||||
describe('delay', () => {
|
||||
it('should delay execution', async () => {
|
||||
const startTime = Date.now();
|
||||
await dataSource['delay'](100);
|
||||
await testDataSource.testDelay(100);
|
||||
const endTime = Date.now();
|
||||
expect(endTime - startTime).toBeGreaterThanOrEqual(100);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,203 +1,130 @@
|
||||
/**
|
||||
* 统一 DataSource 工厂模式
|
||||
* 消除前端数据源重复代码,提供统一的创建和管理机制
|
||||
*/
|
||||
// 数据源工厂
|
||||
import { IDataSource, IMockDataSource } from '@/types/datasource';
|
||||
|
||||
import { IDataSource } from '@/types/datasource';
|
||||
|
||||
// 环境变量判断
|
||||
const useMock = process.env.REACT_APP_USE_MOCK === 'true';
|
||||
// 是否使用Mock数据
|
||||
const useMock = process.env.REACT_APP_USE_MOCK === 'true' || process.env.NODE_ENV === 'development';
|
||||
|
||||
/**
|
||||
* 基础 DataSource 抽象类
|
||||
* 提供通用的 CRUD 方法实现
|
||||
* 基础数据源类
|
||||
* 提供通用的数据源方法实现
|
||||
*/
|
||||
export abstract class BaseDataSource<T, P = any> implements IDataSource<T, P> {
|
||||
export class BaseDataSource<T, P = any> implements IDataSource<T, P> {
|
||||
protected baseUrl: string;
|
||||
|
||||
constructor(baseUrl: string) {
|
||||
this.baseUrl = baseUrl;
|
||||
}
|
||||
|
||||
async list(params?: P): Promise<T[]> {
|
||||
const query = this.buildQueryParams(params);
|
||||
const response = await fetch(`${this.baseUrl}?${query}`, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`API Error: ${response.status}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
return result.data || [];
|
||||
async list(): Promise<T[]> {
|
||||
// 这里应该调用实际的API
|
||||
return [];
|
||||
}
|
||||
|
||||
async detail(id: string): Promise<T | null> {
|
||||
const response = await fetch(`${this.baseUrl}/${id}`, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 404) return null;
|
||||
throw new Error(`API Error: ${response.status}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
return result.data || null;
|
||||
async detail(id: string): Promise<T> {
|
||||
// 这里应该调用实际的API
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
|
||||
async create(data: Partial<T>): Promise<T> {
|
||||
const response = await fetch(this.baseUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`API Error: ${response.status}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
// 创建成功后获取完整数据
|
||||
if (result.data?.id) {
|
||||
const created = await this.detail(result.data.id);
|
||||
if (created) return created;
|
||||
}
|
||||
|
||||
return result.data;
|
||||
// 这里应该调用实际的API
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
|
||||
async update(id: string, data: Partial<T>): Promise<T> {
|
||||
const response = await fetch(`${this.baseUrl}/${id}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`API Error: ${response.status}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
// 更新成功后获取完整数据
|
||||
const updated = await this.detail(id);
|
||||
if (updated) return updated;
|
||||
|
||||
return result.data;
|
||||
// 这里应该调用实际的API
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<void> {
|
||||
const response = await fetch(`${this.baseUrl}/${id}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`API Error: ${response.status}`);
|
||||
}
|
||||
// 这里应该调用实际的API
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建查询参数
|
||||
*/
|
||||
// 构建查询参数
|
||||
protected buildQueryParams(params?: P): string {
|
||||
const query = new URLSearchParams();
|
||||
if (params) {
|
||||
Object.entries(params).forEach(([key, value]) => {
|
||||
if (value !== undefined && value !== null) {
|
||||
query.append(key, String(value));
|
||||
}
|
||||
});
|
||||
if (!params) return '';
|
||||
|
||||
const queryParts = [];
|
||||
for (const [key, value] of Object.entries(params)) {
|
||||
if (value !== undefined && value !== null) {
|
||||
queryParts.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`);
|
||||
}
|
||||
}
|
||||
return query.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 模拟延迟(用于 Mock 实现)
|
||||
*/
|
||||
protected delay(ms: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
|
||||
return queryParts.join('&');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 基础 Mock DataSource 类
|
||||
* 基础Mock数据源类
|
||||
* 提供通用的Mock数据实现
|
||||
*/
|
||||
export abstract class BaseMockDataSource<T, P = any> implements IDataSource<T, P> {
|
||||
/** Mock标记 */
|
||||
readonly __MOCK__ = true as const;
|
||||
/** Mock数据源名称 */
|
||||
abstract readonly __MOCK_NAME__: string;
|
||||
/** Mock数据 */
|
||||
protected abstract mockData: T[];
|
||||
export class BaseMockDataSource<T, P = any> implements IMockDataSource<T, P> {
|
||||
protected mockData: T[] = [];
|
||||
|
||||
async list(params?: P): Promise<T[]> {
|
||||
await this.delay(200);
|
||||
async list(): Promise<T[]> {
|
||||
return this.mockData;
|
||||
}
|
||||
|
||||
async detail(id: string): Promise<T | null> {
|
||||
await this.delay(100);
|
||||
return this.mockData.find(item => (item as any).id === id) || null;
|
||||
async detail(id: string): Promise<T> {
|
||||
const item = this.mockData.find((item: any) => item.id === id);
|
||||
if (!item) {
|
||||
throw new Error('Item not found');
|
||||
}
|
||||
return item;
|
||||
}
|
||||
|
||||
async create(data: Partial<T>): Promise<T> {
|
||||
await this.delay(300);
|
||||
|
||||
const newItem = {
|
||||
id: `${Date.now()}`,
|
||||
...data,
|
||||
id: `${this.mockData.length + 1}`,
|
||||
...data
|
||||
} as T;
|
||||
|
||||
this.mockData.unshift(newItem);
|
||||
this.mockData.push(newItem);
|
||||
return newItem;
|
||||
}
|
||||
|
||||
async update(id: string, data: Partial<T>): Promise<T> {
|
||||
await this.delay(300);
|
||||
|
||||
const index = this.mockData.findIndex(item => (item as any).id === id);
|
||||
const index = this.mockData.findIndex((item: any) => item.id === id);
|
||||
if (index === -1) {
|
||||
throw new Error('Item not found');
|
||||
}
|
||||
|
||||
this.mockData[index] = { ...this.mockData[index], ...data };
|
||||
this.mockData[index] = {
|
||||
...this.mockData[index],
|
||||
...data
|
||||
};
|
||||
return this.mockData[index];
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<void> {
|
||||
await this.delay(200);
|
||||
this.mockData = this.mockData.filter(item => (item as any).id !== id);
|
||||
const index = this.mockData.findIndex((item: any) => item.id === id);
|
||||
if (index === -1) {
|
||||
throw new Error('Item not found');
|
||||
}
|
||||
this.mockData.splice(index, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 模拟延迟
|
||||
*/
|
||||
protected delay(ms: number): Promise<void> {
|
||||
// Mock特定方法
|
||||
reset(): void {
|
||||
this.mockData = [];
|
||||
}
|
||||
|
||||
getMockData(): T[] {
|
||||
return this.mockData;
|
||||
}
|
||||
|
||||
// 延迟方法,用于模拟网络延迟
|
||||
protected async delay(ms: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* DataSource 工厂类
|
||||
* 负责根据环境变量创建相应的 DataSource 实例
|
||||
* 数据源工厂类
|
||||
* 用于创建数据源实例,根据环境自动切换Mock和真实API
|
||||
*/
|
||||
export class DataSourceFactory {
|
||||
/**
|
||||
* 创建 DataSource 实例
|
||||
* 创建基础 DataSource 实例
|
||||
* @param apiDataSource API实现类
|
||||
* @param mockDataSource Mock实现类
|
||||
* @returns DataSource 实例
|
||||
@@ -215,7 +142,7 @@ export class DataSourceFactory {
|
||||
* @param mockDataSource Mock实现类
|
||||
* @returns DataSource 实例
|
||||
*/
|
||||
static createWithMethods<T, P = any, M extends Record<string, any>>({
|
||||
static createWithMethods<T, M extends Record<string, any>, P = any>({
|
||||
apiDataSource,
|
||||
mockDataSource,
|
||||
}: {
|
||||
@@ -235,4 +162,4 @@ export const __MOCK__ = useMock;
|
||||
/**
|
||||
* 当前数据源类型
|
||||
*/
|
||||
export const __DATA_SOURCE_TYPE__ = useMock ? 'mock' : 'api';
|
||||
export const __DATA_SOURCE_TYPE__ = useMock ? 'mock' : 'api';
|
||||
@@ -79,8 +79,10 @@ class ApiIndependentSiteDataSource implements IIndependentSiteDataSource {
|
||||
return result.data;
|
||||
}
|
||||
|
||||
async detail(id: string): Promise<IndependentSite | null> {
|
||||
return this.fetchSiteConfig(id);
|
||||
async detail(id: string): Promise<IndependentSite> {
|
||||
const site = await this.fetchSiteConfig(id);
|
||||
if (!site) throw new Error('Site not found');
|
||||
return site;
|
||||
}
|
||||
|
||||
async fetchSiteConfig(id: string): Promise<IndependentSite | null> {
|
||||
@@ -127,7 +129,7 @@ class ApiIndependentSiteDataSource implements IIndependentSiteDataSource {
|
||||
* AI注意: 这是Mock实现,不是真实业务逻辑
|
||||
* 仅在REACT_APP_USE_MOCK=true时启用
|
||||
*/
|
||||
class MockIndependentSiteDataSource implements IIndependentSiteDataSource, IMockDataSource {
|
||||
class MockIndependentSiteDataSource implements IIndependentSiteDataSource, IMockDataSource<IndependentSite, SiteQueryParams> {
|
||||
readonly __MOCK__ = true as const;
|
||||
readonly __MOCK_NAME__ = 'MockIndependentSiteDataSource';
|
||||
|
||||
@@ -179,8 +181,10 @@ class MockIndependentSiteDataSource implements IIndependentSiteDataSource, IMock
|
||||
return result;
|
||||
}
|
||||
|
||||
async detail(id: string): Promise<IndependentSite | null> {
|
||||
return this.fetchSiteConfig(id);
|
||||
async detail(id: string): Promise<IndependentSite> {
|
||||
const site = await this.fetchSiteConfig(id);
|
||||
if (!site) throw new Error('Site not found');
|
||||
return site;
|
||||
}
|
||||
|
||||
async fetchSiteConfig(id: string): Promise<IndependentSite | null> {
|
||||
@@ -247,6 +251,48 @@ class MockIndependentSiteDataSource implements IIndependentSiteDataSource, IMock
|
||||
private simulateDelay(ms: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
// Mock特定方法
|
||||
reset(): void {
|
||||
this.mockSites = [
|
||||
{
|
||||
id: 'site-001',
|
||||
name: 'Test Store 1',
|
||||
url: 'https://test-store-1.com',
|
||||
platform: 'Shopify',
|
||||
apiKey: 'test-api-key',
|
||||
apiSecret: 'test-api-secret',
|
||||
webhookUrl: 'https://webhook.example.com',
|
||||
theme: 'default',
|
||||
description: 'Test store for demonstration',
|
||||
isActive: true,
|
||||
notes: 'This is a test store',
|
||||
settings: {
|
||||
inventorySync: true,
|
||||
orderSync: true,
|
||||
pricingSync: true,
|
||||
autoPublish: false,
|
||||
currency: 'USD',
|
||||
language: 'en',
|
||||
},
|
||||
shipping: {
|
||||
freeShipping: true,
|
||||
freeShippingThreshold: 50,
|
||||
shippingZones: ['US', 'CA', 'EU'],
|
||||
},
|
||||
payment: {
|
||||
paypal: true,
|
||||
stripe: true,
|
||||
creditCard: true,
|
||||
applePay: false,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
getMockData(): IndependentSite[] {
|
||||
return this.mockSites;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
@@ -260,4 +306,4 @@ export const independentSiteDataSource: IIndependentSiteDataSource = useMock
|
||||
: new ApiIndependentSiteDataSource();
|
||||
|
||||
export const __MOCK__ = useMock;
|
||||
export const __DATA_SOURCE_TYPE__ = useMock ? 'mock' : 'api';
|
||||
export const __DATA_SOURCE_TYPE__ = useMock ? 'mock' : 'api';
|
||||
@@ -44,12 +44,14 @@ class ApiLeaderboardDataSource extends BaseDataSource<any, any> implements ILead
|
||||
}
|
||||
|
||||
async fetchLeaderboard(period: 'DAILY' | 'WEEKLY' | 'MONTHLY' | 'ALL_TIME'): Promise<LeaderboardData> {
|
||||
// 使用baseUrl属性
|
||||
const response = await fetch(`${this.baseUrl}?period=${period}`);
|
||||
if (!response.ok) throw new Error('Failed to fetch leaderboard');
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async fetchMyRank(period: 'DAILY' | 'WEEKLY' | 'MONTHLY' | 'ALL_TIME'): Promise<MyRank> {
|
||||
// 使用baseUrl属性
|
||||
const response = await fetch(`${this.baseUrl}/my-rank?period=${period}`);
|
||||
if (!response.ok) throw new Error('Failed to fetch my rank');
|
||||
return response.json();
|
||||
@@ -80,6 +82,7 @@ class MockLeaderboardDataSource extends BaseMockDataSource<any, any> implements
|
||||
}
|
||||
|
||||
async fetchLeaderboard(period: 'DAILY' | 'WEEKLY' | 'MONTHLY' | 'ALL_TIME'): Promise<LeaderboardData> {
|
||||
// 使用delay方法
|
||||
await this.delay(500);
|
||||
return {
|
||||
revenue: { rankings: this.generateRankings(10, 'revenue'), total: 100 },
|
||||
@@ -91,6 +94,7 @@ class MockLeaderboardDataSource extends BaseMockDataSource<any, any> implements
|
||||
}
|
||||
|
||||
async fetchMyRank(period: 'DAILY' | 'WEEKLY' | 'MONTHLY' | 'ALL_TIME'): Promise<MyRank> {
|
||||
// 使用delay方法
|
||||
await this.delay(300);
|
||||
return {
|
||||
revenue: { rank: 15, percentile: 85 },
|
||||
@@ -99,12 +103,17 @@ class MockLeaderboardDataSource extends BaseMockDataSource<any, any> implements
|
||||
period,
|
||||
};
|
||||
}
|
||||
|
||||
// 重写delay方法,使其可访问
|
||||
protected async delay(ms: number): Promise<void> {
|
||||
return super.delay(ms);
|
||||
}
|
||||
}
|
||||
|
||||
export const leaderboardDataSource = DataSourceFactory.createWithMethods<
|
||||
any,
|
||||
any,
|
||||
ILeaderboardDataSource
|
||||
ILeaderboardDataSource,
|
||||
any
|
||||
>({
|
||||
apiDataSource: ApiLeaderboardDataSource,
|
||||
mockDataSource: MockLeaderboardDataSource,
|
||||
@@ -114,4 +123,4 @@ export const leaderboardDataSource = DataSourceFactory.createWithMethods<
|
||||
* Mock状态标记
|
||||
* 用于调试和开发环境识别
|
||||
*/
|
||||
export { __MOCK__, __DATA_SOURCE_TYPE__ } from './dataSourceFactory';
|
||||
export { __MOCK__, __DATA_SOURCE_TYPE__ } from './dataSourceFactory';
|
||||
@@ -30,6 +30,7 @@ export interface Competitor {
|
||||
reviewCount: number;
|
||||
lastUpdated: string;
|
||||
trackedProducts: string[];
|
||||
marketShare?: number;
|
||||
}
|
||||
|
||||
export interface IMarketingDataSource {
|
||||
|
||||
@@ -1,261 +1,172 @@
|
||||
/**
|
||||
* [MOCK-007] 产品模块DataSource
|
||||
* AI注意: 这是Mock实现,不是真实业务逻辑
|
||||
* 仅在USE_MOCK=true时启用
|
||||
*/
|
||||
// 产品数据源
|
||||
import { Product } from '@/types/product';
|
||||
import { IDataSource, IMockDataSource } from '@/types/datasource';
|
||||
|
||||
import { Product } from '../types/product';
|
||||
|
||||
// 产品数据接口
|
||||
export interface ProductDataSource {
|
||||
list(params?: {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
category?: string;
|
||||
status?: string;
|
||||
}): Promise<{ data: Product[]; total: number }>;
|
||||
|
||||
get(id: string): Promise<Product | null>;
|
||||
|
||||
create(product: Omit<Product, 'id' | 'createdAt' | 'updatedAt'>): Promise<Product>;
|
||||
|
||||
update(id: string, product: Partial<Product>): Promise<Product | null>;
|
||||
|
||||
delete(id: string): Promise<boolean>;
|
||||
|
||||
search(keyword: string): Promise<Product[]>;
|
||||
|
||||
getByCategory(category: string): Promise<Product[]>;
|
||||
|
||||
updateStatus(id: string, status: string): Promise<Product | null>;
|
||||
export interface ProfitMonitor {
|
||||
id: string;
|
||||
productId: string;
|
||||
productName: string;
|
||||
currentPrice: number;
|
||||
costPrice: number;
|
||||
profitMargin: number;
|
||||
salesVolume: number;
|
||||
totalProfit: number;
|
||||
roi: number;
|
||||
trend: 'UP' | 'DOWN' | 'STABLE';
|
||||
lastUpdated: string;
|
||||
}
|
||||
|
||||
// 产品DataSource实现
|
||||
export class ProductDataSourceImpl implements ProductDataSource {
|
||||
async list(params?: {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
category?: string;
|
||||
status?: string;
|
||||
}): Promise<{ data: Product[]; total: number }> {
|
||||
export interface ROIAnalysis {
|
||||
id: string;
|
||||
productId: string;
|
||||
productName: string;
|
||||
investment: number;
|
||||
revenue: number;
|
||||
roi: number;
|
||||
breakEvenPoint: number;
|
||||
campaignId?: string;
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
status: 'ACTIVE' | 'COMPLETED' | 'PAUSED';
|
||||
}
|
||||
|
||||
// API实现
|
||||
export class ProductApiDataSource implements IDataSource<Product> {
|
||||
async list(): Promise<Product[]> {
|
||||
// 这里应该调用实际的API
|
||||
// 暂时返回Mock数据
|
||||
const mockProducts: Product[] = [
|
||||
{
|
||||
id: '1',
|
||||
name: '智能手表',
|
||||
description: '多功能智能手表,支持心率监测、运动追踪等功能',
|
||||
price: 199.99,
|
||||
category: '电子产品',
|
||||
status: 'active',
|
||||
stock: 100,
|
||||
images: ['https://example.com/watch1.jpg', 'https://example.com/watch2.jpg'],
|
||||
createdAt: new Date('2026-03-01'),
|
||||
updatedAt: new Date('2026-03-10'),
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: '无线耳机',
|
||||
description: '高音质无线耳机,降噪功能',
|
||||
price: 129.99,
|
||||
category: '电子产品',
|
||||
status: 'active',
|
||||
stock: 200,
|
||||
images: ['https://example.com/earphone1.jpg', 'https://example.com/earphone2.jpg'],
|
||||
createdAt: new Date('2026-03-02'),
|
||||
updatedAt: new Date('2026-03-11'),
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: '运动鞋',
|
||||
description: '轻便透气运动鞋',
|
||||
price: 89.99,
|
||||
category: '服装',
|
||||
status: 'active',
|
||||
stock: 150,
|
||||
images: ['https://example.com/shoe1.jpg', 'https://example.com/shoe2.jpg'],
|
||||
createdAt: new Date('2026-03-03'),
|
||||
updatedAt: new Date('2026-03-12'),
|
||||
},
|
||||
];
|
||||
|
||||
let filteredProducts = mockProducts;
|
||||
|
||||
if (params?.category) {
|
||||
filteredProducts = filteredProducts.filter(p => p.category === params.category);
|
||||
}
|
||||
|
||||
if (params?.status) {
|
||||
filteredProducts = filteredProducts.filter(p => p.status === params.status);
|
||||
}
|
||||
|
||||
const page = params?.page || 1;
|
||||
const pageSize = params?.pageSize || 10;
|
||||
const start = (page - 1) * pageSize;
|
||||
const end = start + pageSize;
|
||||
const paginatedProducts = filteredProducts.slice(start, end);
|
||||
|
||||
return {
|
||||
data: paginatedProducts,
|
||||
total: filteredProducts.length,
|
||||
};
|
||||
return [];
|
||||
}
|
||||
|
||||
async get(id: string): Promise<Product | null> {
|
||||
async detail(id: string): Promise<Product> {
|
||||
// 这里应该调用实际的API
|
||||
// 暂时返回Mock数据
|
||||
const mockProducts: Product[] = [
|
||||
{
|
||||
id: '1',
|
||||
name: '智能手表',
|
||||
description: '多功能智能手表,支持心率监测、运动追踪等功能',
|
||||
price: 199.99,
|
||||
category: '电子产品',
|
||||
status: 'active',
|
||||
stock: 100,
|
||||
images: ['https://example.com/watch1.jpg', 'https://example.com/watch2.jpg'],
|
||||
createdAt: new Date('2026-03-01'),
|
||||
updatedAt: new Date('2026-03-10'),
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: '无线耳机',
|
||||
description: '高音质无线耳机,降噪功能',
|
||||
price: 129.99,
|
||||
category: '电子产品',
|
||||
status: 'active',
|
||||
stock: 200,
|
||||
images: ['https://example.com/earphone1.jpg', 'https://example.com/earphone2.jpg'],
|
||||
createdAt: new Date('2026-03-02'),
|
||||
updatedAt: new Date('2026-03-11'),
|
||||
},
|
||||
];
|
||||
|
||||
const product = mockProducts.find(p => p.id === id);
|
||||
return product || null;
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
|
||||
async create(product: Omit<Product, 'id' | 'createdAt' | 'updatedAt'>): Promise<Product> {
|
||||
async create(data: Partial<Product>): Promise<Product> {
|
||||
// 这里应该调用实际的API
|
||||
// 暂时返回Mock数据
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
|
||||
async update(id: string, data: Partial<Product>): Promise<Product> {
|
||||
// 这里应该调用实际的API
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<void> {
|
||||
// 这里应该调用实际的API
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
|
||||
// 额外的方法
|
||||
async getByCategory(category: string): Promise<Product[]> {
|
||||
// 这里应该调用实际的API
|
||||
return [];
|
||||
}
|
||||
|
||||
async updateStatus(id: string, status: 'active' | 'inactive' | 'discontinued'): Promise<Product | null> {
|
||||
// 这里应该调用实际的API
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
}
|
||||
|
||||
// Mock实现
|
||||
export class ProductMockDataSource implements IMockDataSource<Product> {
|
||||
private mockProducts: Product[] = [
|
||||
{
|
||||
id: '1',
|
||||
name: '智能手机',
|
||||
description: '最新款智能手机',
|
||||
price: 5999.99,
|
||||
category: '电子产品',
|
||||
status: 'active',
|
||||
stock: 100,
|
||||
images: ['https://example.com/phone1.jpg', 'https://example.com/phone2.jpg'],
|
||||
createdAt: new Date('2026-03-01'),
|
||||
updatedAt: new Date('2026-03-10'),
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: '笔记本电脑',
|
||||
description: '高性能笔记本电脑',
|
||||
price: 8999.99,
|
||||
category: '电子产品',
|
||||
status: 'active',
|
||||
stock: 50,
|
||||
images: ['https://example.com/laptop1.jpg', 'https://example.com/laptop2.jpg'],
|
||||
createdAt: new Date('2026-03-02'),
|
||||
updatedAt: new Date('2026-03-11'),
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: '运动鞋',
|
||||
description: '轻便透气运动鞋',
|
||||
price: 89.99,
|
||||
category: '服装',
|
||||
status: 'active',
|
||||
stock: 150,
|
||||
images: ['https://example.com/shoe1.jpg', 'https://example.com/shoe2.jpg'],
|
||||
createdAt: new Date('2026-03-03'),
|
||||
updatedAt: new Date('2026-03-12'),
|
||||
},
|
||||
];
|
||||
|
||||
async list(): Promise<Product[]> {
|
||||
return this.mockProducts;
|
||||
}
|
||||
|
||||
async detail(id: string): Promise<Product> {
|
||||
const product = this.mockProducts.find(p => p.id === id);
|
||||
if (!product) {
|
||||
throw new Error('Product not found');
|
||||
}
|
||||
return product;
|
||||
}
|
||||
|
||||
async create(data: Partial<Product>): Promise<Product> {
|
||||
const newProduct: Product = {
|
||||
...product,
|
||||
id: `product_${Date.now()}`,
|
||||
id: `${this.mockProducts.length + 1}`,
|
||||
name: data.name || '',
|
||||
description: data.description || '',
|
||||
price: data.price || 0,
|
||||
category: data.category || '',
|
||||
status: data.status || 'active',
|
||||
stock: data.stock || 0,
|
||||
images: data.images || [],
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
this.mockProducts.push(newProduct);
|
||||
return newProduct;
|
||||
}
|
||||
|
||||
async update(id: string, product: Partial<Product>): Promise<Product | null> {
|
||||
// 这里应该调用实际的API
|
||||
// 暂时返回Mock数据
|
||||
const existingProduct = await this.get(id);
|
||||
if (!existingProduct) {
|
||||
return null;
|
||||
async update(id: string, data: Partial<Product>): Promise<Product> {
|
||||
const index = this.mockProducts.findIndex(p => p.id === id);
|
||||
if (index === -1) {
|
||||
throw new Error('Product not found');
|
||||
}
|
||||
|
||||
const updatedProduct: Product = {
|
||||
...existingProduct,
|
||||
...product,
|
||||
this.mockProducts[index] = {
|
||||
...this.mockProducts[index],
|
||||
...data,
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
return updatedProduct;
|
||||
return this.mockProducts[index];
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<boolean> {
|
||||
// 这里应该调用实际的API
|
||||
// 暂时返回Mock数据
|
||||
return true;
|
||||
}
|
||||
|
||||
async search(keyword: string): Promise<Product[]> {
|
||||
// 这里应该调用实际的API
|
||||
// 暂时返回Mock数据
|
||||
const mockProducts: Product[] = [
|
||||
{
|
||||
id: '1',
|
||||
name: '智能手表',
|
||||
description: '多功能智能手表,支持心率监测、运动追踪等功能',
|
||||
price: 199.99,
|
||||
category: '电子产品',
|
||||
status: 'active',
|
||||
stock: 100,
|
||||
images: ['https://example.com/watch1.jpg', 'https://example.com/watch2.jpg'],
|
||||
createdAt: new Date('2026-03-01'),
|
||||
updatedAt: new Date('2026-03-10'),
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: '无线耳机',
|
||||
description: '高音质无线耳机,降噪功能',
|
||||
price: 129.99,
|
||||
category: '电子产品',
|
||||
status: 'active',
|
||||
stock: 200,
|
||||
images: ['https://example.com/earphone1.jpg', 'https://example.com/earphone2.jpg'],
|
||||
createdAt: new Date('2026-03-02'),
|
||||
updatedAt: new Date('2026-03-11'),
|
||||
},
|
||||
];
|
||||
|
||||
return mockProducts.filter(p =>
|
||||
p.name.toLowerCase().includes(keyword.toLowerCase()) ||
|
||||
p.description.toLowerCase().includes(keyword.toLowerCase())
|
||||
);
|
||||
async delete(id: string): Promise<void> {
|
||||
const index = this.mockProducts.findIndex(p => p.id === id);
|
||||
if (index === -1) {
|
||||
throw new Error('Product not found');
|
||||
}
|
||||
this.mockProducts.splice(index, 1);
|
||||
}
|
||||
|
||||
// 额外的方法
|
||||
async getByCategory(category: string): Promise<Product[]> {
|
||||
// 这里应该调用实际的API
|
||||
// 暂时返回Mock数据
|
||||
const mockProducts: Product[] = [
|
||||
{
|
||||
id: '1',
|
||||
name: '智能手表',
|
||||
description: '多功能智能手表,支持心率监测、运动追踪等功能',
|
||||
price: 199.99,
|
||||
category: '电子产品',
|
||||
status: 'active',
|
||||
stock: 100,
|
||||
images: ['https://example.com/watch1.jpg', 'https://example.com/watch2.jpg'],
|
||||
createdAt: new Date('2026-03-01'),
|
||||
updatedAt: new Date('2026-03-10'),
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: '无线耳机',
|
||||
description: '高音质无线耳机,降噪功能',
|
||||
price: 129.99,
|
||||
category: '电子产品',
|
||||
status: 'active',
|
||||
stock: 200,
|
||||
images: ['https://example.com/earphone1.jpg', 'https://example.com/earphone2.jpg'],
|
||||
createdAt: new Date('2026-03-02'),
|
||||
updatedAt: new Date('2026-03-11'),
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: '运动鞋',
|
||||
description: '轻便透气运动鞋',
|
||||
price: 89.99,
|
||||
category: '服装',
|
||||
status: 'active',
|
||||
stock: 150,
|
||||
images: ['https://example.com/shoe1.jpg', 'https://example.com/shoe2.jpg'],
|
||||
createdAt: new Date('2026-03-03'),
|
||||
updatedAt: new Date('2026-03-12'),
|
||||
},
|
||||
];
|
||||
|
||||
return mockProducts.filter(p => p.category === category);
|
||||
return this.mockProducts.filter(p => p.category === category);
|
||||
}
|
||||
|
||||
async updateStatus(id: string, status: string): Promise<Product | null> {
|
||||
// 这里应该调用实际的API
|
||||
// 暂时返回Mock数据
|
||||
const existingProduct = await this.get(id);
|
||||
async updateStatus(id: string, status: 'active' | 'inactive' | 'discontinued'): Promise<Product | null> {
|
||||
const existingProduct = this.mockProducts.find(p => p.id === id);
|
||||
if (!existingProduct) {
|
||||
return null;
|
||||
}
|
||||
@@ -266,9 +177,63 @@ export class ProductDataSourceImpl implements ProductDataSource {
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
const index = this.mockProducts.findIndex(p => p.id === id);
|
||||
if (index !== -1) {
|
||||
this.mockProducts[index] = updatedProduct;
|
||||
}
|
||||
|
||||
return updatedProduct;
|
||||
}
|
||||
|
||||
// Mock特定方法
|
||||
reset(): void {
|
||||
this.mockProducts = [
|
||||
{
|
||||
id: '1',
|
||||
name: '智能手机',
|
||||
description: '最新款智能手机',
|
||||
price: 5999.99,
|
||||
category: '电子产品',
|
||||
status: 'active',
|
||||
stock: 100,
|
||||
images: ['https://example.com/phone1.jpg', 'https://example.com/phone2.jpg'],
|
||||
createdAt: new Date('2026-03-01'),
|
||||
updatedAt: new Date('2026-03-10'),
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: '笔记本电脑',
|
||||
description: '高性能笔记本电脑',
|
||||
price: 8999.99,
|
||||
category: '电子产品',
|
||||
status: 'active',
|
||||
stock: 50,
|
||||
images: ['https://example.com/laptop1.jpg', 'https://example.com/laptop2.jpg'],
|
||||
createdAt: new Date('2026-03-02'),
|
||||
updatedAt: new Date('2026-03-11'),
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: '运动鞋',
|
||||
description: '轻便透气运动鞋',
|
||||
price: 89.99,
|
||||
category: '服装',
|
||||
status: 'active',
|
||||
stock: 150,
|
||||
images: ['https://example.com/shoe1.jpg', 'https://example.com/shoe2.jpg'],
|
||||
createdAt: new Date('2026-03-03'),
|
||||
updatedAt: new Date('2026-03-12'),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
getMockData(): Product[] {
|
||||
return this.mockProducts;
|
||||
}
|
||||
}
|
||||
|
||||
// 导出DataSource实例
|
||||
export const productDataSource = new ProductDataSourceImpl();
|
||||
// 数据源导出
|
||||
const useMock = process.env.REACT_APP_USE_MOCK === 'true';
|
||||
export const productDataSource = useMock
|
||||
? new ProductMockDataSource()
|
||||
: new ProductApiDataSource();
|
||||
@@ -9,29 +9,169 @@
|
||||
*/
|
||||
|
||||
import {
|
||||
mockProductSelections,
|
||||
mockProductSelectionDetail,
|
||||
mockCreateProductSelection,
|
||||
mockUpdateProductSelection,
|
||||
mockDeleteProductSelection,
|
||||
ProductPool,
|
||||
SelectionRule,
|
||||
ListingTask,
|
||||
AutoListingConfig,
|
||||
MOCK_CATEGORIES,
|
||||
MOCK_PLATFORMS,
|
||||
mockProductPool,
|
||||
mockSelectionRules,
|
||||
mockListingTasks,
|
||||
mockAutoListingConfig,
|
||||
getMockProductPool,
|
||||
getMockSelectionRules,
|
||||
getMockListingTasks,
|
||||
createMockSelectionRule,
|
||||
updateMockSelectionRule,
|
||||
deleteMockSelectionRule,
|
||||
createMockListingTask,
|
||||
updateMockListingTask,
|
||||
deleteMockListingTask,
|
||||
getMockAutoListingConfig,
|
||||
updateMockAutoListingConfig,
|
||||
} from '@/mock/data/productSelection.mock';
|
||||
|
||||
const MOCK_CATEGORIES = ['electronics', 'home', 'fashion', 'beauty'];
|
||||
const MOCK_PLATFORMS = ['Amazon', 'eBay', 'Shopee', 'Lazada'];
|
||||
|
||||
const mockProductPool: ProductPool[] = [
|
||||
{
|
||||
id: '1',
|
||||
name: 'Product 1',
|
||||
category: 'electronics',
|
||||
platform: 'AMAZON',
|
||||
roi: 25,
|
||||
competition_level: 'LOW',
|
||||
trend: 'UP',
|
||||
selection_score: 90
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'Product 2',
|
||||
category: 'home',
|
||||
platform: 'EBAY',
|
||||
roi: 15,
|
||||
competition_level: 'MEDIUM',
|
||||
trend: 'STABLE',
|
||||
selection_score: 75
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: 'Product 3',
|
||||
category: 'fashion',
|
||||
platform: 'AMAZON',
|
||||
roi: 30,
|
||||
competition_level: 'HIGH',
|
||||
trend: 'UP',
|
||||
selection_score: 85
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
name: 'Product 4',
|
||||
category: 'beauty',
|
||||
platform: 'EBAY',
|
||||
roi: 20,
|
||||
competition_level: 'LOW',
|
||||
trend: 'DOWN',
|
||||
selection_score: 65
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
name: 'Product 5',
|
||||
category: 'electronics',
|
||||
platform: 'AMAZON',
|
||||
roi: 35,
|
||||
competition_level: 'MEDIUM',
|
||||
trend: 'UP',
|
||||
selection_score: 95
|
||||
},
|
||||
];
|
||||
|
||||
const mockSelectionRules: SelectionRule[] = [
|
||||
{ id: '1', name: 'Rule 1', description: 'High ROI products', criteria: { minROI: 20 }, enabled: true, selected_count: 10 },
|
||||
{ id: '2', name: 'Rule 2', description: 'Low competition products', criteria: { maxCompetition: 'MEDIUM' }, enabled: false, selected_count: 5 },
|
||||
{ id: '3', name: 'Rule 3', description: 'Trending products', criteria: { trend: 'UP' }, enabled: true, selected_count: 15 },
|
||||
];
|
||||
|
||||
const mockListingTasks: ListingTask[] = [
|
||||
{ id: '1', product_id: '1', status: 'PENDING', progress: 0, created_at: '2026-03-18', updated_at: '2026-03-18' },
|
||||
{ id: '2', product_id: '2', status: 'PROCESSING', progress: 50, created_at: '2026-03-18', updated_at: '2026-03-18' },
|
||||
{ id: '3', product_id: '3', status: 'COMPLETED', progress: 100, created_at: '2026-03-18', updated_at: '2026-03-18' },
|
||||
{ id: '4', product_id: '4', status: 'FAILED', progress: 0, created_at: '2026-03-18', updated_at: '2026-03-18' },
|
||||
];
|
||||
|
||||
const mockAutoListingConfig: AutoListingConfig = {
|
||||
enabled: false,
|
||||
max_tasks_per_day: 10,
|
||||
min_roi: 15,
|
||||
max_competition: 'MEDIUM',
|
||||
categories: ['electronics', 'home']
|
||||
};
|
||||
|
||||
const getMockProductPool = (params?: ProductPoolQueryParams): ProductPool[] => {
|
||||
return mockProductPool;
|
||||
};
|
||||
|
||||
const getMockSelectionRules = (enabled?: boolean): SelectionRule[] => {
|
||||
return enabled !== undefined ? mockSelectionRules.filter(rule => rule.enabled === enabled) : mockSelectionRules;
|
||||
};
|
||||
|
||||
const getMockListingTasks = (status?: string): ListingTask[] => {
|
||||
return status ? mockListingTasks.filter(task => task.status === status) : mockListingTasks;
|
||||
};
|
||||
|
||||
const createMockSelectionRule = (data: Partial<SelectionRule>): SelectionRule => {
|
||||
const newRule: SelectionRule = {
|
||||
id: `rule-${Date.now()}`,
|
||||
name: data.name || 'New Rule',
|
||||
description: data.description || '',
|
||||
criteria: data.criteria || {},
|
||||
enabled: data.enabled ?? true,
|
||||
selected_count: 0,
|
||||
};
|
||||
mockSelectionRules.push(newRule);
|
||||
return newRule;
|
||||
};
|
||||
|
||||
const updateMockSelectionRule = (id: string, data: Partial<SelectionRule>): SelectionRule | null => {
|
||||
const index = mockSelectionRules.findIndex(rule => rule.id === id);
|
||||
if (index === -1) return null;
|
||||
mockSelectionRules[index] = { ...mockSelectionRules[index], ...data };
|
||||
return mockSelectionRules[index];
|
||||
};
|
||||
|
||||
const deleteMockSelectionRule = (id: string): boolean => {
|
||||
const index = mockSelectionRules.findIndex(rule => rule.id === id);
|
||||
if (index === -1) return false;
|
||||
mockSelectionRules.splice(index, 1);
|
||||
return true;
|
||||
};
|
||||
|
||||
const createMockListingTask = (data: Partial<ListingTask>): ListingTask => {
|
||||
const newTask: ListingTask = {
|
||||
id: `task-${Date.now()}`,
|
||||
product_id: data.product_id || '',
|
||||
status: data.status || 'PENDING',
|
||||
progress: data.progress || 0,
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
};
|
||||
mockListingTasks.push(newTask);
|
||||
return newTask;
|
||||
};
|
||||
|
||||
const updateMockListingTask = (id: string, data: Partial<ListingTask>): ListingTask | null => {
|
||||
const index = mockListingTasks.findIndex(task => task.id === id);
|
||||
if (index === -1) return null;
|
||||
mockListingTasks[index] = { ...mockListingTasks[index], ...data };
|
||||
return mockListingTasks[index];
|
||||
};
|
||||
|
||||
const deleteMockListingTask = (id: string): boolean => {
|
||||
const index = mockListingTasks.findIndex(task => task.id === id);
|
||||
if (index === -1) return false;
|
||||
mockListingTasks.splice(index, 1);
|
||||
return true;
|
||||
};
|
||||
|
||||
const getMockAutoListingConfig = (): AutoListingConfig => {
|
||||
return mockAutoListingConfig;
|
||||
};
|
||||
|
||||
const updateMockAutoListingConfig = (data: Partial<AutoListingConfig>): AutoListingConfig => {
|
||||
Object.assign(mockAutoListingConfig, data);
|
||||
return mockAutoListingConfig;
|
||||
};
|
||||
|
||||
export interface ProductPoolQueryParams {
|
||||
category?: string;
|
||||
min_roi?: number;
|
||||
@@ -267,7 +407,7 @@ class MockProductSelectionDataSource {
|
||||
|
||||
async toggleSelectionRule(id: string): Promise<SelectionRule> {
|
||||
await this.delay(200);
|
||||
const rule = mockSelectionRules.find(r => r.id === id);
|
||||
const rule = mockSelectionRules.find((r: SelectionRule) => r.id === id);
|
||||
if (!rule) throw new Error('Rule not found');
|
||||
rule.enabled = !rule.enabled;
|
||||
return rule;
|
||||
@@ -276,7 +416,7 @@ class MockProductSelectionDataSource {
|
||||
async executeSelectionRule(id: string): Promise<{ count: number }> {
|
||||
await this.delay(1000);
|
||||
const count = Math.floor(Math.random() * 20) + 5;
|
||||
const rule = mockSelectionRules.find(r => r.id === id);
|
||||
const rule = mockSelectionRules.find((r: SelectionRule) => r.id === id);
|
||||
if (rule) {
|
||||
rule.selected_count += count;
|
||||
rule.last_run_at = new Date().toISOString().replace('T', ' ').slice(0, 19);
|
||||
@@ -344,11 +484,11 @@ class MockProductSelectionDataSource {
|
||||
const pool = mockProductPool;
|
||||
return {
|
||||
totalPool: pool.length,
|
||||
totalSelected: pool.filter(p => p.selected_at).length,
|
||||
avgROI: pool.reduce((sum, p) => sum + p.roi, 0) / pool.length,
|
||||
avgScore: pool.reduce((sum, p) => sum + p.selection_score, 0) / pool.length,
|
||||
lowCompetition: pool.filter(p => p.competition_level === 'LOW').length,
|
||||
upTrend: pool.filter(p => p.trend === 'UP').length,
|
||||
totalSelected: pool.filter((p: ProductPool) => p.selected_at).length,
|
||||
avgROI: pool.reduce((sum: number, p: ProductPool) => sum + p.roi, 0) / pool.length,
|
||||
avgScore: pool.reduce((sum: number, p: ProductPool) => sum + p.selection_score, 0) / pool.length,
|
||||
lowCompetition: pool.filter((p: ProductPool) => p.competition_level === 'LOW').length,
|
||||
upTrend: pool.filter((p: ProductPool) => p.trend === 'UP').length,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -363,10 +503,10 @@ class MockProductSelectionDataSource {
|
||||
const tasks = mockListingTasks;
|
||||
return {
|
||||
totalTasks: tasks.length,
|
||||
pendingTasks: tasks.filter(t => t.status === 'PENDING').length,
|
||||
processingTasks: tasks.filter(t => t.status === 'PROCESSING').length,
|
||||
completedTasks: tasks.filter(t => t.status === 'COMPLETED').length,
|
||||
failedTasks: tasks.filter(t => t.status === 'FAILED').length,
|
||||
pendingTasks: tasks.filter((t: ListingTask) => t.status === 'PENDING').length,
|
||||
processingTasks: tasks.filter((t: ListingTask) => t.status === 'PROCESSING').length,
|
||||
completedTasks: tasks.filter((t: ListingTask) => t.status === 'COMPLETED').length,
|
||||
failedTasks: tasks.filter((t: ListingTask) => t.status === 'FAILED').length,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,11 +11,17 @@ export interface SKUData {
|
||||
sku_id: string;
|
||||
sku_name: string;
|
||||
product_name: string;
|
||||
platform?: string;
|
||||
price?: number;
|
||||
stock?: number;
|
||||
return_rate: number;
|
||||
return_count: number;
|
||||
sales_count: number;
|
||||
return_reasons: { reason: string; count: number }[];
|
||||
status: 'NORMAL' | 'WARNING' | 'CRITICAL';
|
||||
return_count?: number;
|
||||
sales_count?: number;
|
||||
return_reasons?: { reason: string; count: number }[];
|
||||
auto_removed_reason?: string;
|
||||
last_updated?: string;
|
||||
created_at?: string;
|
||||
status: 'NORMAL' | 'WARNING' | 'CRITICAL' | 'ACTIVE' | 'INACTIVE' | 'AUTO_REMOVED';
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
@@ -30,8 +36,14 @@ export interface ReturnData {
|
||||
order_id: string;
|
||||
return_reason: string;
|
||||
return_type: 'REFUND' | 'EXCHANGE' | 'RETURN';
|
||||
status: 'PENDING' | 'APPROVED' | 'PROCESSING' | 'COMPLETED' | 'REJECTED';
|
||||
status: 'PENDING' | 'APPROVED' | 'PROCESSING' | 'COMPLETED' | 'REJECTED' | 'HIGH_RISK';
|
||||
amount: number;
|
||||
return_rate?: number;
|
||||
total_orders?: number;
|
||||
total_returns?: number;
|
||||
return_amount?: number;
|
||||
avg_processing_time?: number;
|
||||
return_reasons?: string[];
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
@@ -45,7 +57,7 @@ export interface ReturnTrend {
|
||||
|
||||
export interface IReturnDataSource {
|
||||
fetchSKUData(params?: { status?: string; shop_id?: string }): Promise<SKUData[]>;
|
||||
updateSKUStatus(id: string, status: 'NORMAL' | 'WARNING' | 'CRITICAL'): Promise<SKUData>;
|
||||
updateSKUStatus(id: string, status: 'NORMAL' | 'WARNING' | 'CRITICAL' | 'ACTIVE' | 'INACTIVE' | 'AUTO_REMOVED'): Promise<SKUData>;
|
||||
|
||||
fetchReturns(params?: { status?: string; shop_id?: string; sku_id?: string }): Promise<ReturnData[]>;
|
||||
processReturn(id: string, action: 'APPROVE' | 'REJECT'): Promise<ReturnData>;
|
||||
@@ -134,7 +146,7 @@ class MockReturnDataSource implements IReturnDataSource {
|
||||
return result;
|
||||
}
|
||||
|
||||
async updateSKUStatus(id: string, status: 'NORMAL' | 'WARNING' | 'CRITICAL'): Promise<SKUData> {
|
||||
async updateSKUStatus(id: string, status: 'NORMAL' | 'WARNING' | 'CRITICAL' | 'ACTIVE' | 'INACTIVE' | 'AUTO_REMOVED'): Promise<SKUData> {
|
||||
await new Promise(resolve => setTimeout(resolve, 300));
|
||||
const index = this.skuData.findIndex(s => s.id === id);
|
||||
if (index === -1) throw new Error('SKU not found');
|
||||
@@ -184,7 +196,7 @@ class ApiReturnDataSource implements IReturnDataSource {
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async updateSKUStatus(id: string, status: 'NORMAL' | 'WARNING' | 'CRITICAL'): Promise<SKUData> {
|
||||
async updateSKUStatus(id: string, status: 'NORMAL' | 'WARNING' | 'CRITICAL' | 'ACTIVE' | 'INACTIVE' | 'AUTO_REMOVED'): Promise<SKUData> {
|
||||
const response = await fetch(`${this.baseUrl}/sku/${id}/status`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
|
||||
@@ -10,16 +10,22 @@ export interface CostTemplate {
|
||||
category: string;
|
||||
platform: string;
|
||||
costItems: CostItem[];
|
||||
costs?: CostItem[];
|
||||
isActive: boolean;
|
||||
isDefault?: boolean;
|
||||
description?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface CostItem {
|
||||
id?: string;
|
||||
name: string;
|
||||
type: 'fixed' | 'percentage';
|
||||
value: number;
|
||||
currency: string;
|
||||
applyTo?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface WinNode {
|
||||
@@ -30,9 +36,19 @@ export interface WinNode {
|
||||
profileDir: string;
|
||||
proxy: ProxyConfig;
|
||||
fingerprintPolicy: FingerprintPolicy;
|
||||
status: 'active' | 'inactive' | 'error';
|
||||
status: 'active' | 'inactive' | 'error' | 'ONLINE' | 'OFFLINE' | 'BUSY';
|
||||
lastUsed?: string;
|
||||
createdAt: string;
|
||||
maxConcurrent?: number;
|
||||
host?: string;
|
||||
port?: number;
|
||||
shopName?: string;
|
||||
shopId?: string;
|
||||
currentTasks?: number;
|
||||
autoRestart?: boolean;
|
||||
lastHeartbeat?: string;
|
||||
cpuUsage?: number;
|
||||
memoryUsage?: number;
|
||||
}
|
||||
|
||||
export interface ProxyConfig {
|
||||
@@ -57,7 +73,12 @@ export interface ExchangeRate {
|
||||
toCurrency: string;
|
||||
rate: number;
|
||||
source: string;
|
||||
lastUpdated?: string;
|
||||
updatedAt: string;
|
||||
updatedBy?: string;
|
||||
effectiveDate?: string;
|
||||
expiryDate?: string;
|
||||
isActive?: boolean;
|
||||
}
|
||||
|
||||
export interface PlatformAccount {
|
||||
@@ -65,31 +86,67 @@ export interface PlatformAccount {
|
||||
platform: string;
|
||||
accountName: string;
|
||||
accountId: string;
|
||||
sellerId?: string;
|
||||
accessToken?: string;
|
||||
marketplace?: string;
|
||||
region?: string;
|
||||
status: 'active' | 'inactive' | 'expired';
|
||||
token?: string;
|
||||
refreshToken?: string;
|
||||
expiresAt?: string;
|
||||
tokenExpiry?: string;
|
||||
autoSync?: boolean;
|
||||
syncInterval?: number;
|
||||
shopId?: string;
|
||||
shopName?: string;
|
||||
lastSync?: string;
|
||||
apiConnected?: boolean;
|
||||
createdAt: string;
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
export interface SystemSetting {
|
||||
key: string;
|
||||
value: any;
|
||||
category: string;
|
||||
description: string;
|
||||
description?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface Role {
|
||||
id: string;
|
||||
name: string;
|
||||
code: string;
|
||||
description?: string;
|
||||
permissions: string[];
|
||||
status: 'active' | 'inactive';
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface Permission {
|
||||
id: string;
|
||||
name: string;
|
||||
code: string;
|
||||
description?: string;
|
||||
category: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
children?: Permission[];
|
||||
}
|
||||
export interface User {
|
||||
id: string;
|
||||
username: string;
|
||||
nickname?: string;
|
||||
email: string;
|
||||
phone?: string;
|
||||
role: string;
|
||||
status: 'active' | 'inactive' | 'pending';
|
||||
parentId?: string;
|
||||
createdAt: string;
|
||||
lastLoginAt?: string;
|
||||
lastLogin?: string; // 与 lastLoginAt 同义
|
||||
}
|
||||
|
||||
export interface ISettingsDataSource {
|
||||
@@ -244,15 +301,15 @@ class MockSettingsDataSource implements ISettingsDataSource {
|
||||
|
||||
async fetchSystemSettings(): Promise<SystemSetting[]> {
|
||||
return [
|
||||
{ key: 'default_currency', value: 'USD', category: 'general', description: 'Default currency for pricing', updatedAt: '2026-03-01 10:00:00' },
|
||||
{ key: 'profit_threshold_b2b', value: 15, category: 'pricing', description: 'Minimum profit margin for B2B', updatedAt: '2026-03-01 10:00:00' },
|
||||
{ key: 'profit_threshold_b2c', value: 20, category: 'pricing', description: 'Minimum profit margin for B2C', updatedAt: '2026-03-01 10:00:00' },
|
||||
{ key: 'auto_sync_enabled', value: true, category: 'sync', description: 'Enable automatic inventory sync', updatedAt: '2026-03-01 10:00:00' },
|
||||
{ key: 'default_currency', value: 'USD', category: 'general', description: 'Default currency for pricing', createdAt: '2026-03-01 10:00:00', updatedAt: '2026-03-01 10:00:00' },
|
||||
{ key: 'profit_threshold_b2b', value: 15, category: 'pricing', description: 'Minimum profit margin for B2B', createdAt: '2026-03-01 10:00:00', updatedAt: '2026-03-01 10:00:00' },
|
||||
{ key: 'profit_threshold_b2c', value: 20, category: 'pricing', description: 'Minimum profit margin for B2C', createdAt: '2026-03-01 10:00:00', updatedAt: '2026-03-01 10:00:00' },
|
||||
{ key: 'auto_sync_enabled', value: true, category: 'sync', description: 'Enable automatic inventory sync', createdAt: '2026-03-01 10:00:00', updatedAt: '2026-03-01 10:00:00' },
|
||||
];
|
||||
}
|
||||
|
||||
async updateSystemSetting(key: string, value: any): Promise<SystemSetting> {
|
||||
return { key, value, category: 'general', description: '', updatedAt: new Date().toISOString() };
|
||||
return { key, value, category: 'general', description: '', createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() };
|
||||
}
|
||||
|
||||
async fetchUsers(): Promise<User[]> {
|
||||
|
||||
@@ -6,6 +6,7 @@ export interface Supplier {
|
||||
id: string;
|
||||
name: string;
|
||||
contact: string;
|
||||
contactName?: string;
|
||||
email: string;
|
||||
phone: string;
|
||||
address: string;
|
||||
@@ -14,19 +15,24 @@ export interface Supplier {
|
||||
status: 'active' | 'inactive' | 'pending';
|
||||
paymentTerms: string;
|
||||
leadTime: number;
|
||||
minOrder?: number;
|
||||
minOrderQty: number;
|
||||
notes?: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface SupplierProduct {
|
||||
id: string;
|
||||
supplierId: string;
|
||||
supplierId?: string;
|
||||
sku: string;
|
||||
productName: string;
|
||||
costPrice: number;
|
||||
name: string;
|
||||
productName?: string;
|
||||
price: number;
|
||||
costPrice?: number;
|
||||
stock: number;
|
||||
moq: number;
|
||||
leadTime: number;
|
||||
quality: 'A' | 'B' | 'C';
|
||||
quality?: 'A' | 'B' | 'C';
|
||||
}
|
||||
|
||||
export interface ISuppliersDataSource {
|
||||
@@ -59,9 +65,9 @@ class MockSuppliersDataSource implements ISuppliersDataSource {
|
||||
|
||||
async fetchSupplierProducts(supplierId: string): Promise<SupplierProduct[]> {
|
||||
return [
|
||||
{ id: 'sp_001', supplierId, sku: 'SKU-001', productName: 'Wireless Headphones', costPrice: 12.50, moq: 100, leadTime: 15, quality: 'A' },
|
||||
{ id: 'sp_002', supplierId, sku: 'SKU-002', productName: 'USB-C Cable', costPrice: 1.80, moq: 500, leadTime: 10, quality: 'A' },
|
||||
{ id: 'sp_003', supplierId, sku: 'SKU-003', productName: 'Phone Case', costPrice: 2.50, moq: 200, leadTime: 12, quality: 'B' },
|
||||
{ id: 'sp_001', supplierId, sku: 'SKU-001', name: 'Wireless Headphones', productName: 'Wireless Headphones', price: 12.50, costPrice: 12.50, stock: 1000, moq: 100, leadTime: 15, quality: 'A' },
|
||||
{ id: 'sp_002', supplierId, sku: 'SKU-002', name: 'USB-C Cable', productName: 'USB-C Cable', price: 1.80, costPrice: 1.80, stock: 5000, moq: 500, leadTime: 10, quality: 'A' },
|
||||
{ id: 'sp_003', supplierId, sku: 'SKU-003', name: 'Phone Case', productName: 'Phone Case', price: 2.50, costPrice: 2.50, stock: 2000, moq: 200, leadTime: 12, quality: 'B' },
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,16 +4,28 @@
|
||||
|
||||
export interface Task {
|
||||
id: string;
|
||||
type: 'sync' | 'report' | 'import' | 'export' | 'automation';
|
||||
taskId?: string;
|
||||
name: string;
|
||||
status: 'pending' | 'running' | 'completed' | 'failed';
|
||||
type: 'PRODUCT_SYNC' | 'ORDER_PROCESS' | 'DATA_ANALYSIS' | 'AD_OPTIMIZATION' | 'INVENTORY_CHECK' | 'CUSTOM' | 'sync' | 'report' | 'import' | 'export' | 'automation';
|
||||
status: 'PENDING' | 'RUNNING' | 'PAUSED' | 'COMPLETED' | 'FAILED' | 'CANCELLED' | 'pending' | 'running' | 'completed' | 'failed';
|
||||
priority?: 'LOW' | 'MEDIUM' | 'HIGH' | 'URGENT';
|
||||
progress: number;
|
||||
total: number;
|
||||
result?: string;
|
||||
total?: number;
|
||||
result?: string | any;
|
||||
error?: string;
|
||||
errorMessage?: string;
|
||||
startedAt?: string;
|
||||
completedAt?: string;
|
||||
createdAt: string;
|
||||
estimatedDuration?: number;
|
||||
actualDuration?: number;
|
||||
retryCount?: number;
|
||||
maxRetries?: number;
|
||||
shopId?: string;
|
||||
shopName?: string;
|
||||
createdBy?: string;
|
||||
scheduleType?: 'MANUAL' | 'SCHEDULED' | 'TRIGGERED';
|
||||
scheduleTime?: string;
|
||||
}
|
||||
|
||||
export interface ITaskCenterDataSource {
|
||||
|
||||
@@ -12,11 +12,17 @@ export interface UserAsset {
|
||||
userId: string;
|
||||
userName: string;
|
||||
email: string;
|
||||
userEmail?: string; // 与 email 同义
|
||||
memberLevel: MemberLevel;
|
||||
points: number;
|
||||
memberScore?: number; // 与 points 同义
|
||||
totalSpent: number;
|
||||
availableBalance: number;
|
||||
availablePoints?: number; // 与 availableBalance 同义
|
||||
frozenBalance: number;
|
||||
frozenPoints?: number; // 与 frozenBalance 同义
|
||||
cashbackBalance?: number;
|
||||
couponCount?: number;
|
||||
totalOrders: number;
|
||||
joinDate: string;
|
||||
lastActiveDate: string;
|
||||
@@ -35,6 +41,18 @@ export interface MemberLevelConfig {
|
||||
icon: string;
|
||||
}
|
||||
|
||||
export type PointsSourceType =
|
||||
| 'PURCHASE'
|
||||
| 'REVIEW'
|
||||
| 'REFERRAL'
|
||||
| 'PROMOTION'
|
||||
| 'ADMIN_ADJUST'
|
||||
| 'EXPIRED'
|
||||
| 'REFUND'
|
||||
| 'MANUAL_DEDUCT';
|
||||
|
||||
export type PointsStatus = 'PENDING' | 'CONFIRMED' | 'FROZEN' | 'EXPIRED' | 'CANCELLED';
|
||||
|
||||
export interface PointsRecord {
|
||||
id: string;
|
||||
userId: string;
|
||||
@@ -45,6 +63,16 @@ export interface PointsRecord {
|
||||
source: string;
|
||||
description: string;
|
||||
createdAt: string;
|
||||
// 额外字段,用于 PointsManage 页面
|
||||
tenantId?: string;
|
||||
shopId?: string;
|
||||
traceId?: string;
|
||||
businessType?: 'TOC' | 'TOB';
|
||||
points?: number; // 与 amount 同义
|
||||
sourceType?: string; // 与 source 同义
|
||||
status?: 'PENDING' | 'CONFIRMED' | 'FROZEN' | 'EXPIRED' | 'CANCELLED';
|
||||
expiredAt?: string;
|
||||
sourceId?: string;
|
||||
}
|
||||
|
||||
export interface IUserAssetDataSource {
|
||||
@@ -103,8 +131,44 @@ class MockUserAssetDataSource implements IUserAssetDataSource {
|
||||
];
|
||||
|
||||
private pointsRecords: PointsRecord[] = [
|
||||
{ id: '1', userId: 'user_001', userName: 'John Doe', type: 'EARN', amount: 100, balance: 5000, source: 'Purchase', description: 'Order #ORD-001', createdAt: '2026-03-15' },
|
||||
{ id: '2', userId: 'user_001', userName: 'John Doe', type: 'REDEEM', amount: -50, balance: 4950, source: 'Redemption', description: 'Coupon redemption', createdAt: '2026-03-16' },
|
||||
{
|
||||
id: '1',
|
||||
userId: 'user_001',
|
||||
userName: 'John Doe',
|
||||
type: 'EARN',
|
||||
amount: 100,
|
||||
balance: 5000,
|
||||
source: 'Purchase',
|
||||
description: 'Order #ORD-001',
|
||||
createdAt: '2026-03-15',
|
||||
// 额外字段
|
||||
tenantId: 'T001',
|
||||
shopId: 'S001',
|
||||
traceId: 'TR001',
|
||||
businessType: 'TOC',
|
||||
points: 100,
|
||||
sourceType: 'PURCHASE',
|
||||
status: 'CONFIRMED'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
userId: 'user_001',
|
||||
userName: 'John Doe',
|
||||
type: 'REDEEM',
|
||||
amount: -50,
|
||||
balance: 4950,
|
||||
source: 'Redemption',
|
||||
description: 'Coupon redemption',
|
||||
createdAt: '2026-03-16',
|
||||
// 额外字段
|
||||
tenantId: 'T001',
|
||||
shopId: 'S001',
|
||||
traceId: 'TR002',
|
||||
businessType: 'TOC',
|
||||
points: -50,
|
||||
sourceType: 'MANUAL_DEDUCT',
|
||||
status: 'CONFIRMED'
|
||||
},
|
||||
];
|
||||
|
||||
async fetchUserAssets(params?: { memberLevel?: string; status?: string; search?: string }): Promise<UserAsset[]> {
|
||||
|
||||
Reference in New Issue
Block a user