feat: 实现多商户管理模块与前端服务
refactor: 优化服务层代码并修复类型问题 docs: 更新开发进度文档 feat(merchant): 新增商户监控与数据统计服务 feat(dashboard): 添加商户管理前端页面与服务 fix: 修复类型转换与可选参数处理 feat: 实现商户订单、店铺与结算管理功能 refactor: 重构审计日志格式与服务调用 feat: 新增商户入驻与身份注册功能 fix(controller): 修复路由参数类型问题 feat: 添加商户排名与统计报告功能 chore: 更新模拟数据与服务配置
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -19,25 +19,31 @@ if (process.env.NODE_ENV === 'development') {
|
||||
import React from 'react';
|
||||
|
||||
export async function getRoutes() {
|
||||
const routes = {"Compliance/CertificateExpiryReminder":{"path":"Compliance/CertificateExpiryReminder","id":"Compliance/CertificateExpiryReminder"},"Compliance/CertificateManage":{"path":"Compliance/CertificateManage","id":"Compliance/CertificateManage"},"AfterSales/CustomerService":{"path":"AfterSales/CustomerService","id":"AfterSales/CustomerService"},"Compliance/ComplianceCheck":{"path":"Compliance/ComplianceCheck","id":"Compliance/ComplianceCheck"},"Product/ProductPublishForm":{"path":"Product/ProductPublishForm","id":"Product/ProductPublishForm"},"Logistics/LogisticsSelect":{"path":"Logistics/LogisticsSelect","id":"Logistics/LogisticsSelect"},"AfterSales/RefundProcess":{"path":"AfterSales/RefundProcess","id":"AfterSales/RefundProcess"},"B2BTrade/EnterpriseQuote":{"path":"B2BTrade/EnterpriseQuote","id":"B2BTrade/EnterpriseQuote"},"Logistics/LogisticsTrack":{"path":"Logistics/LogisticsTrack","id":"Logistics/LogisticsTrack"},"B2BTrade/ContractManage":{"path":"B2BTrade/ContractManage","id":"B2BTrade/ContractManage"},"Orders/OrderAggregation":{"path":"Orders/OrderAggregation","id":"Orders/OrderAggregation"},"AfterSales/ReturnApply":{"path":"AfterSales/ReturnApply","id":"AfterSales/ReturnApply"},"Auth/ResetPasswordPage":{"path":"Auth/ResetPasswordPage","id":"Auth/ResetPasswordPage"},"Product/MaterialUpload":{"path":"Product/MaterialUpload","id":"Product/MaterialUpload"},"UserAsset/PointsManage":{"path":"UserAsset/PointsManage","id":"UserAsset/PointsManage"},"Logistics/FreightCalc":{"path":"Logistics/FreightCalc","id":"Logistics/FreightCalc"},"Orders/ExceptionOrder":{"path":"Orders/ExceptionOrder","id":"Orders/ExceptionOrder"},"Product/ProductDetail":{"path":"Product/ProductDetail","id":"Product/ProductDetail"},"UserAsset/MemberLevel":{"path":"UserAsset/MemberLevel","id":"UserAsset/MemberLevel"},"ABTest/ABTestResults":{"path":"ABTest/ABTestResults","id":"ABTest/ABTestResults"},"UserAsset/UserAssets":{"path":"UserAsset/UserAssets","id":"UserAsset/UserAssets"},"ABTest/ABTestConfig":{"path":"ABTest/ABTestConfig","id":"ABTest/ABTestConfig"},"B2B/EnterpriseQuote":{"path":"B2B/EnterpriseQuote","id":"B2B/EnterpriseQuote"},"B2BTrade/BatchOrder":{"path":"B2BTrade/BatchOrder","id":"B2BTrade/BatchOrder"},"B2B/ContractManage":{"path":"B2B/ContractManage","id":"B2B/ContractManage"},"Orders/OrderDetail":{"path":"Orders/OrderDetail","id":"Orders/OrderDetail"},"Auth/RegisterPage":{"path":"Auth/RegisterPage","id":"Auth/RegisterPage"},"AfterSales/index":{"path":"AfterSales","id":"AfterSales/index"},"Compliance/index":{"path":"Compliance","id":"Compliance/index"},"Orders/OrderList":{"path":"Orders/OrderList","id":"Orders/OrderList"},"Logistics/index":{"path":"Logistics","id":"Logistics/index"},"UserAsset/index":{"path":"UserAsset","id":"UserAsset/index"},"Ad/ROIAnalysis":{"path":"Ad/ROIAnalysis","id":"Ad/ROIAnalysis"},"Auth/LoginPage":{"path":"Auth/LoginPage","id":"Auth/LoginPage"},"B2B/BatchOrder":{"path":"B2B/BatchOrder","id":"B2B/BatchOrder"},"B2BTrade/index":{"path":"B2BTrade","id":"B2BTrade/index"},"Ad/AdDelivery":{"path":"Ad/AdDelivery","id":"Ad/AdDelivery"},"Ad/AdPlanPage":{"path":"Ad/AdPlanPage","id":"Ad/AdPlanPage"},"Product/index":{"path":"Product","id":"Product/index"},"ABTest/index":{"path":"ABTest","id":"ABTest/index"},"Orders/index":{"path":"Orders","id":"Orders/index"},"Auth/index":{"path":"Auth","id":"Auth/index"},"B2B/index":{"path":"B2B","id":"B2B/index"},"Ad/index":{"path":"Ad","id":"Ad/index"}} as const;
|
||||
const routes = {"Compliance/CertificateExpiryReminder":{"path":"Compliance/CertificateExpiryReminder","id":"Compliance/CertificateExpiryReminder"},"Merchant/MerchantSettlementManage":{"path":"Merchant/MerchantSettlementManage","id":"Merchant/MerchantSettlementManage"},"Compliance/CertificateManage":{"path":"Compliance/CertificateManage","id":"Compliance/CertificateManage"},"Merchant/MerchantOrderManage":{"path":"Merchant/MerchantOrderManage","id":"Merchant/MerchantOrderManage"},"Merchant/MerchantShopManage":{"path":"Merchant/MerchantShopManage","id":"Merchant/MerchantShopManage"},"AfterSales/CustomerService":{"path":"AfterSales/CustomerService","id":"AfterSales/CustomerService"},"Compliance/ComplianceCheck":{"path":"Compliance/ComplianceCheck","id":"Compliance/ComplianceCheck"},"Product/ProductPublishForm":{"path":"Product/ProductPublishForm","id":"Product/ProductPublishForm"},"Blacklist/BlacklistManage":{"path":"Blacklist/BlacklistManage","id":"Blacklist/BlacklistManage"},"Logistics/LogisticsSelect":{"path":"Logistics/LogisticsSelect","id":"Logistics/LogisticsSelect"},"AfterSales/RefundProcess":{"path":"AfterSales/RefundProcess","id":"AfterSales/RefundProcess"},"B2BTrade/EnterpriseQuote":{"path":"B2BTrade/EnterpriseQuote","id":"B2BTrade/EnterpriseQuote"},"Logistics/LogisticsTrack":{"path":"Logistics/LogisticsTrack","id":"Logistics/LogisticsTrack"},"B2BTrade/ContractManage":{"path":"B2BTrade/ContractManage","id":"B2BTrade/ContractManage"},"Merchant/MerchantManage":{"path":"Merchant/MerchantManage","id":"Merchant/MerchantManage"},"Orders/OrderAggregation":{"path":"Orders/OrderAggregation","id":"Orders/OrderAggregation"},"AfterSales/ReturnApply":{"path":"AfterSales/ReturnApply","id":"AfterSales/ReturnApply"},"Auth/ResetPasswordPage":{"path":"Auth/ResetPasswordPage","id":"Auth/ResetPasswordPage"},"Product/MaterialUpload":{"path":"Product/MaterialUpload","id":"Product/MaterialUpload"},"UserAsset/PointsManage":{"path":"UserAsset/PointsManage","id":"UserAsset/PointsManage"},"Blacklist/RiskMonitor":{"path":"Blacklist/RiskMonitor","id":"Blacklist/RiskMonitor"},"Logistics/FreightCalc":{"path":"Logistics/FreightCalc","id":"Logistics/FreightCalc"},"Orders/ExceptionOrder":{"path":"Orders/ExceptionOrder","id":"Orders/ExceptionOrder"},"Product/ProductDetail":{"path":"Product/ProductDetail","id":"Product/ProductDetail"},"UserAsset/MemberLevel":{"path":"UserAsset/MemberLevel","id":"UserAsset/MemberLevel"},"ABTest/ABTestResults":{"path":"ABTest/ABTestResults","id":"ABTest/ABTestResults"},"UserAsset/UserAssets":{"path":"UserAsset/UserAssets","id":"UserAsset/UserAssets"},"ABTest/ABTestConfig":{"path":"ABTest/ABTestConfig","id":"ABTest/ABTestConfig"},"B2B/EnterpriseQuote":{"path":"B2B/EnterpriseQuote","id":"B2B/EnterpriseQuote"},"B2BTrade/BatchOrder":{"path":"B2BTrade/BatchOrder","id":"B2BTrade/BatchOrder"},"B2B/ContractManage":{"path":"B2B/ContractManage","id":"B2B/ContractManage"},"Orders/OrderDetail":{"path":"Orders/OrderDetail","id":"Orders/OrderDetail"},"Auth/RegisterPage":{"path":"Auth/RegisterPage","id":"Auth/RegisterPage"},"AfterSales/index":{"path":"AfterSales","id":"AfterSales/index"},"Compliance/index":{"path":"Compliance","id":"Compliance/index"},"Orders/OrderList":{"path":"Orders/OrderList","id":"Orders/OrderList"},"Logistics/index":{"path":"Logistics","id":"Logistics/index"},"UserAsset/index":{"path":"UserAsset","id":"UserAsset/index"},"Ad/ROIAnalysis":{"path":"Ad/ROIAnalysis","id":"Ad/ROIAnalysis"},"Auth/LoginPage":{"path":"Auth/LoginPage","id":"Auth/LoginPage"},"B2B/BatchOrder":{"path":"B2B/BatchOrder","id":"B2B/BatchOrder"},"B2BTrade/index":{"path":"B2BTrade","id":"B2BTrade/index"},"Merchant/index":{"path":"Merchant","id":"Merchant/index"},"Ad/AdDelivery":{"path":"Ad/AdDelivery","id":"Ad/AdDelivery"},"Ad/AdPlanPage":{"path":"Ad/AdPlanPage","id":"Ad/AdPlanPage"},"Product/index":{"path":"Product","id":"Product/index"},"ABTest/index":{"path":"ABTest","id":"ABTest/index"},"Orders/index":{"path":"Orders","id":"Orders/index"},"Auth/index":{"path":"Auth","id":"Auth/index"},"B2B/index":{"path":"B2B","id":"B2B/index"},"Ad/index":{"path":"Ad","id":"Ad/index"}} as const;
|
||||
return {
|
||||
routes,
|
||||
routeComponents: {
|
||||
'Compliance/CertificateExpiryReminder': React.lazy(() => import(/* webpackChunkName: "src__pages__Compliance__CertificateExpiryReminder" */'../../../src/pages/Compliance/CertificateExpiryReminder.tsx')),
|
||||
'Merchant/MerchantSettlementManage': React.lazy(() => import(/* webpackChunkName: "src__pages__Merchant__MerchantSettlementManage" */'../../../src/pages/Merchant/MerchantSettlementManage.tsx')),
|
||||
'Compliance/CertificateManage': React.lazy(() => import(/* webpackChunkName: "src__pages__Compliance__CertificateManage" */'../../../src/pages/Compliance/CertificateManage.tsx')),
|
||||
'Merchant/MerchantOrderManage': React.lazy(() => import(/* webpackChunkName: "src__pages__Merchant__MerchantOrderManage" */'../../../src/pages/Merchant/MerchantOrderManage.tsx')),
|
||||
'Merchant/MerchantShopManage': React.lazy(() => import(/* webpackChunkName: "src__pages__Merchant__MerchantShopManage" */'../../../src/pages/Merchant/MerchantShopManage.tsx')),
|
||||
'AfterSales/CustomerService': React.lazy(() => import(/* webpackChunkName: "src__pages__AfterSales__CustomerService" */'../../../src/pages/AfterSales/CustomerService.tsx')),
|
||||
'Compliance/ComplianceCheck': React.lazy(() => import(/* webpackChunkName: "src__pages__Compliance__ComplianceCheck" */'../../../src/pages/Compliance/ComplianceCheck.tsx')),
|
||||
'Product/ProductPublishForm': React.lazy(() => import(/* webpackChunkName: "src__pages__Product__ProductPublishForm" */'../../../src/pages/Product/ProductPublishForm.tsx')),
|
||||
'Blacklist/BlacklistManage': React.lazy(() => import(/* webpackChunkName: "src__pages__Blacklist__BlacklistManage" */'../../../src/pages/Blacklist/BlacklistManage.tsx')),
|
||||
'Logistics/LogisticsSelect': React.lazy(() => import(/* webpackChunkName: "src__pages__Logistics__LogisticsSelect" */'../../../src/pages/Logistics/LogisticsSelect.tsx')),
|
||||
'AfterSales/RefundProcess': React.lazy(() => import(/* webpackChunkName: "src__pages__AfterSales__RefundProcess" */'../../../src/pages/AfterSales/RefundProcess.tsx')),
|
||||
'B2BTrade/EnterpriseQuote': React.lazy(() => import(/* webpackChunkName: "src__pages__B2BTrade__EnterpriseQuote" */'../../../src/pages/B2BTrade/EnterpriseQuote.tsx')),
|
||||
'Logistics/LogisticsTrack': React.lazy(() => import(/* webpackChunkName: "src__pages__Logistics__LogisticsTrack" */'../../../src/pages/Logistics/LogisticsTrack.tsx')),
|
||||
'B2BTrade/ContractManage': React.lazy(() => import(/* webpackChunkName: "src__pages__B2BTrade__ContractManage" */'../../../src/pages/B2BTrade/ContractManage.tsx')),
|
||||
'Merchant/MerchantManage': React.lazy(() => import(/* webpackChunkName: "src__pages__Merchant__MerchantManage" */'../../../src/pages/Merchant/MerchantManage.tsx')),
|
||||
'Orders/OrderAggregation': React.lazy(() => import(/* webpackChunkName: "src__pages__Orders__OrderAggregation" */'../../../src/pages/Orders/OrderAggregation.tsx')),
|
||||
'AfterSales/ReturnApply': React.lazy(() => import(/* webpackChunkName: "src__pages__AfterSales__ReturnApply" */'../../../src/pages/AfterSales/ReturnApply.tsx')),
|
||||
'Auth/ResetPasswordPage': React.lazy(() => import(/* webpackChunkName: "src__pages__Auth__ResetPasswordPage" */'../../../src/pages/Auth/ResetPasswordPage.tsx')),
|
||||
'Product/MaterialUpload': React.lazy(() => import(/* webpackChunkName: "src__pages__Product__MaterialUpload" */'../../../src/pages/Product/MaterialUpload.tsx')),
|
||||
'UserAsset/PointsManage': React.lazy(() => import(/* webpackChunkName: "src__pages__UserAsset__PointsManage" */'../../../src/pages/UserAsset/PointsManage.tsx')),
|
||||
'Blacklist/RiskMonitor': React.lazy(() => import(/* webpackChunkName: "src__pages__Blacklist__RiskMonitor" */'../../../src/pages/Blacklist/RiskMonitor.tsx')),
|
||||
'Logistics/FreightCalc': React.lazy(() => import(/* webpackChunkName: "src__pages__Logistics__FreightCalc" */'../../../src/pages/Logistics/FreightCalc.tsx')),
|
||||
'Orders/ExceptionOrder': React.lazy(() => import(/* webpackChunkName: "src__pages__Orders__ExceptionOrder" */'../../../src/pages/Orders/ExceptionOrder.tsx')),
|
||||
'Product/ProductDetail': React.lazy(() => import(/* webpackChunkName: "src__pages__Product__ProductDetail" */'../../../src/pages/Product/ProductDetail.tsx')),
|
||||
@@ -59,6 +65,7 @@ export async function getRoutes() {
|
||||
'Auth/LoginPage': React.lazy(() => import(/* webpackChunkName: "src__pages__Auth__LoginPage" */'../../../src/pages/Auth/LoginPage.tsx')),
|
||||
'B2B/BatchOrder': React.lazy(() => import(/* webpackChunkName: "src__pages__B2B__BatchOrder" */'../../../src/pages/B2B/BatchOrder.tsx')),
|
||||
'B2BTrade/index': React.lazy(() => import(/* webpackChunkName: "src__pages__B2BTrade__index" */'../../../src/pages/B2BTrade/index.ts')),
|
||||
'Merchant/index': React.lazy(() => import(/* webpackChunkName: "src__pages__Merchant__index" */'../../../src/pages/Merchant/index.ts')),
|
||||
'Ad/AdDelivery': React.lazy(() => import(/* webpackChunkName: "src__pages__Ad__AdDelivery" */'../../../src/pages/Ad/AdDelivery.tsx')),
|
||||
'Ad/AdPlanPage': React.lazy(() => import(/* webpackChunkName: "src__pages__Ad__AdPlanPage" */'../../../src/pages/Ad/AdPlanPage.tsx')),
|
||||
'Product/index': React.lazy(() => import(/* webpackChunkName: "src__pages__Product__index" */'../../../src/pages/Product/index.ts')),
|
||||
|
||||
829
dashboard/src/pages/Blacklist/BlacklistManage.tsx
Normal file
829
dashboard/src/pages/Blacklist/BlacklistManage.tsx
Normal file
@@ -0,0 +1,829 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Card,
|
||||
Table,
|
||||
Button,
|
||||
Space,
|
||||
Modal,
|
||||
Form,
|
||||
Input,
|
||||
Select,
|
||||
DatePicker,
|
||||
InputNumber,
|
||||
Descriptions,
|
||||
Divider,
|
||||
message,
|
||||
Tag,
|
||||
Tabs,
|
||||
Row,
|
||||
Col,
|
||||
Statistic,
|
||||
Alert,
|
||||
Tooltip,
|
||||
Badge,
|
||||
Upload,
|
||||
} from 'antd';
|
||||
import {
|
||||
UserOutlined,
|
||||
PlusOutlined,
|
||||
EyeOutlined,
|
||||
EditOutlined,
|
||||
DeleteOutlined,
|
||||
SearchOutlined,
|
||||
WarningOutlined,
|
||||
CheckCircleOutlined,
|
||||
CloseCircleOutlined,
|
||||
StopOutlined,
|
||||
ExclamationCircleOutlined,
|
||||
UploadOutlined,
|
||||
DownloadOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
const { Option } = Select;
|
||||
const { TabPane } = Tabs;
|
||||
const { TextArea } = Input;
|
||||
|
||||
interface BlacklistRecord {
|
||||
id: string;
|
||||
tenant_id: string;
|
||||
shop_id: string;
|
||||
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';
|
||||
evidence?: string;
|
||||
created_by: string;
|
||||
trace_id: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
const BLACKLIST_TYPES = [
|
||||
{ value: 'FRAUD', label: 'Fraud', color: 'red' },
|
||||
{ value: 'CHARGEBACK', label: 'Chargeback', color: 'orange' },
|
||||
{ value: 'ABUSE', label: 'Abuse', color: 'purple' },
|
||||
{ value: 'OTHER', label: 'Other', color: 'default' },
|
||||
];
|
||||
|
||||
const STATUS_CONFIG: Record<string, { color: string; text: string; icon: React.ReactNode }> = {
|
||||
ACTIVE: { color: 'error', text: 'Active', icon: <StopOutlined /> },
|
||||
INACTIVE: { color: 'default', text: 'Inactive', icon: <CloseCircleOutlined /> },
|
||||
EXPIRED: { color: 'default', text: 'Expired', icon: <ExclamationCircleOutlined /> },
|
||||
};
|
||||
|
||||
const PLATFORMS = [
|
||||
'Amazon',
|
||||
'eBay',
|
||||
'Shopify',
|
||||
'Walmart',
|
||||
'TikTok Shop',
|
||||
'Temu',
|
||||
'Alibaba',
|
||||
'Other',
|
||||
];
|
||||
|
||||
const MOCK_BLACKLISTS: BlacklistRecord[] = [
|
||||
{
|
||||
id: '1',
|
||||
tenant_id: 'tenant-001',
|
||||
shop_id: 'shop-001',
|
||||
buyer_id: 'buyer-001',
|
||||
buyer_name: 'John Smith',
|
||||
buyer_email: 'john.smith@example.com',
|
||||
buyer_phone: '+1-555-0101',
|
||||
platform: 'Amazon',
|
||||
platform_buyer_id: 'AMZ-001',
|
||||
blacklist_reason: 'Multiple fraudulent transactions',
|
||||
blacklist_type: 'FRAUD',
|
||||
risk_score: 85,
|
||||
blacklist_date: '2026-01-15',
|
||||
expiry_date: '2027-01-15',
|
||||
status: 'ACTIVE',
|
||||
evidence: 'Transaction records, IP logs',
|
||||
created_by: 'admin',
|
||||
trace_id: 'trace-001',
|
||||
created_at: '2026-01-15 10:00:00',
|
||||
updated_at: '2026-01-15 10:00:00',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
tenant_id: 'tenant-001',
|
||||
shop_id: 'shop-001',
|
||||
buyer_id: 'buyer-002',
|
||||
buyer_name: 'Jane Doe',
|
||||
buyer_email: 'jane.doe@example.com',
|
||||
platform: 'eBay',
|
||||
platform_buyer_id: 'EBAY-002',
|
||||
blacklist_reason: 'Excessive chargebacks',
|
||||
blacklist_type: 'CHARGEBACK',
|
||||
risk_score: 75,
|
||||
blacklist_date: '2026-02-20',
|
||||
expiry_date: '2026-08-20',
|
||||
status: 'ACTIVE',
|
||||
evidence: 'Chargeback documentation',
|
||||
created_by: 'admin',
|
||||
trace_id: 'trace-002',
|
||||
created_at: '2026-02-20 14:30:00',
|
||||
updated_at: '2026-02-20 14:30:00',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
tenant_id: 'tenant-001',
|
||||
shop_id: 'shop-001',
|
||||
buyer_id: 'buyer-003',
|
||||
buyer_name: 'Mike Johnson',
|
||||
buyer_email: 'mike.j@example.com',
|
||||
platform: 'Amazon',
|
||||
platform_buyer_id: 'AMZ-003',
|
||||
blacklist_reason: 'Abusive behavior towards support',
|
||||
blacklist_type: 'ABUSE',
|
||||
risk_score: 60,
|
||||
blacklist_date: '2026-03-01',
|
||||
expiry_date: '2026-06-01',
|
||||
status: 'ACTIVE',
|
||||
evidence: 'Support chat logs',
|
||||
created_by: 'admin',
|
||||
trace_id: 'trace-003',
|
||||
created_at: '2026-03-01 09:15:00',
|
||||
updated_at: '2026-03-01 09:15:00',
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
tenant_id: 'tenant-001',
|
||||
shop_id: 'shop-001',
|
||||
buyer_id: 'buyer-004',
|
||||
buyer_name: 'Sarah Wilson',
|
||||
buyer_email: 'sarah.w@example.com',
|
||||
platform: 'Shopify',
|
||||
platform_buyer_id: 'SHOPIFY-004',
|
||||
blacklist_reason: 'Suspicious ordering pattern',
|
||||
blacklist_type: 'OTHER',
|
||||
risk_score: 50,
|
||||
blacklist_date: '2026-02-10',
|
||||
expiry_date: '2026-05-10',
|
||||
status: 'INACTIVE',
|
||||
evidence: 'Order history analysis',
|
||||
created_by: 'admin',
|
||||
trace_id: 'trace-004',
|
||||
created_at: '2026-02-10 11:00:00',
|
||||
updated_at: '2026-03-01 16:00:00',
|
||||
},
|
||||
];
|
||||
|
||||
const BlacklistManage: React.FC = () => {
|
||||
const [form] = Form.useForm();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [blacklists, setBlacklists] = useState<BlacklistRecord[]>(MOCK_BLACKLISTS);
|
||||
const [createModalVisible, setCreateModalVisible] = useState(false);
|
||||
const [editModalVisible, setEditModalVisible] = useState(false);
|
||||
const [detailModalVisible, setDetailModalVisible] = useState(false);
|
||||
const [selectedBlacklist, setSelectedBlacklist] = useState<BlacklistRecord | null>(null);
|
||||
const [activeTab, setActiveTab] = useState('all');
|
||||
const [searchText, setSearchText] = useState('');
|
||||
const [platformFilter, setPlatformFilter] = useState<string>('');
|
||||
const [typeFilter, setTypeFilter] = useState<string>('');
|
||||
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchBlacklists();
|
||||
}, []);
|
||||
|
||||
const fetchBlacklists = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
setBlacklists(MOCK_BLACKLISTS);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreate = async (values: any) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const newBlacklist: BlacklistRecord = {
|
||||
id: `${Date.now()}`,
|
||||
tenant_id: 'tenant-001',
|
||||
shop_id: 'shop-001',
|
||||
buyer_id: `buyer-${Date.now()}`,
|
||||
buyer_name: values.buyer_name,
|
||||
buyer_email: values.buyer_email,
|
||||
buyer_phone: values.buyer_phone,
|
||||
platform: values.platform,
|
||||
platform_buyer_id: values.platform_buyer_id,
|
||||
blacklist_reason: values.blacklist_reason,
|
||||
blacklist_type: values.blacklist_type,
|
||||
risk_score: values.risk_score,
|
||||
blacklist_date: dayjs().format('YYYY-MM-DD'),
|
||||
expiry_date: values.expiry_date ? values.expiry_date.format('YYYY-MM-DD') : undefined,
|
||||
status: 'ACTIVE',
|
||||
evidence: values.evidence,
|
||||
created_by: 'admin',
|
||||
trace_id: `trace-${Date.now()}`,
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
};
|
||||
|
||||
setBlacklists([newBlacklist, ...blacklists]);
|
||||
message.success('Buyer added to blacklist successfully');
|
||||
setCreateModalVisible(false);
|
||||
form.resetFields();
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEdit = async (values: any) => {
|
||||
if (!selectedBlacklist) return;
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const updatedBlacklists = blacklists.map(bl =>
|
||||
bl.id === selectedBlacklist.id
|
||||
? {
|
||||
...bl,
|
||||
buyer_name: values.buyer_name,
|
||||
buyer_email: values.buyer_email,
|
||||
buyer_phone: values.buyer_phone,
|
||||
blacklist_reason: values.blacklist_reason,
|
||||
blacklist_type: values.blacklist_type,
|
||||
risk_score: values.risk_score,
|
||||
expiry_date: values.expiry_date ? values.expiry_date.format('YYYY-MM-DD') : undefined,
|
||||
status: values.status,
|
||||
evidence: values.evidence,
|
||||
updated_at: new Date().toISOString(),
|
||||
}
|
||||
: bl
|
||||
);
|
||||
|
||||
setBlacklists(updatedBlacklists);
|
||||
message.success('Blacklist record updated successfully');
|
||||
setEditModalVisible(false);
|
||||
form.resetFields();
|
||||
setSelectedBlacklist(null);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = (id: string) => {
|
||||
Modal.confirm({
|
||||
title: 'Remove from Blacklist',
|
||||
content: 'Are you sure you want to remove this buyer from the blacklist?',
|
||||
okType: 'danger',
|
||||
onOk: () => {
|
||||
const updatedBlacklists = blacklists.map(bl =>
|
||||
bl.id === id ? { ...bl, status: 'INACTIVE' as const, updated_at: new Date().toISOString() } : bl
|
||||
);
|
||||
setBlacklists(updatedBlacklists);
|
||||
message.success('Buyer removed from blacklist');
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleBatchDelete = () => {
|
||||
if (selectedRowKeys.length === 0) {
|
||||
message.warning('Please select records to delete');
|
||||
return;
|
||||
}
|
||||
|
||||
Modal.confirm({
|
||||
title: 'Batch Remove from Blacklist',
|
||||
content: `Are you sure you want to remove ${selectedRowKeys.length} buyers from the blacklist?`,
|
||||
okType: 'danger',
|
||||
onOk: () => {
|
||||
const updatedBlacklists = blacklists.map(bl =>
|
||||
selectedRowKeys.includes(bl.id) ? { ...bl, status: 'INACTIVE' as const, updated_at: new Date().toISOString() } : bl
|
||||
);
|
||||
setBlacklists(updatedBlacklists);
|
||||
setSelectedRowKeys([]);
|
||||
message.success(`${selectedRowKeys.length} buyers removed from blacklist`);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleViewDetail = (blacklist: BlacklistRecord) => {
|
||||
setSelectedBlacklist(blacklist);
|
||||
setDetailModalVisible(true);
|
||||
};
|
||||
|
||||
const handleEditClick = (blacklist: BlacklistRecord) => {
|
||||
setSelectedBlacklist(blacklist);
|
||||
form.setFieldsValue({
|
||||
buyer_name: blacklist.buyer_name,
|
||||
buyer_email: blacklist.buyer_email,
|
||||
buyer_phone: blacklist.buyer_phone,
|
||||
platform: blacklist.platform,
|
||||
platform_buyer_id: blacklist.platform_buyer_id,
|
||||
blacklist_reason: blacklist.blacklist_reason,
|
||||
blacklist_type: blacklist.blacklist_type,
|
||||
risk_score: blacklist.risk_score,
|
||||
expiry_date: blacklist.expiry_date ? dayjs(blacklist.expiry_date) : null,
|
||||
status: blacklist.status,
|
||||
evidence: blacklist.evidence,
|
||||
});
|
||||
setEditModalVisible(true);
|
||||
};
|
||||
|
||||
const getFilteredBlacklists = () => {
|
||||
let filtered = blacklists;
|
||||
|
||||
if (activeTab !== 'all') {
|
||||
filtered = filtered.filter(bl => bl.status === activeTab.toUpperCase());
|
||||
}
|
||||
|
||||
if (searchText) {
|
||||
filtered = filtered.filter(bl =>
|
||||
bl.buyer_name.toLowerCase().includes(searchText.toLowerCase()) ||
|
||||
bl.buyer_email.toLowerCase().includes(searchText.toLowerCase()) ||
|
||||
bl.platform_buyer_id.toLowerCase().includes(searchText.toLowerCase())
|
||||
);
|
||||
}
|
||||
|
||||
if (platformFilter) {
|
||||
filtered = filtered.filter(bl => bl.platform === platformFilter);
|
||||
}
|
||||
|
||||
if (typeFilter) {
|
||||
filtered = filtered.filter(bl => bl.blacklist_type === typeFilter);
|
||||
}
|
||||
|
||||
return filtered;
|
||||
};
|
||||
|
||||
const getRiskLevel = (score: number): { level: string; color: string } => {
|
||||
if (score >= 80) return { level: 'Critical', color: '#ff4d4f' };
|
||||
if (score >= 60) return { level: 'High', color: '#ff7a45' };
|
||||
if (score >= 40) return { level: 'Medium', color: '#faad14' };
|
||||
return { level: 'Low', color: '#52c41a' };
|
||||
};
|
||||
|
||||
const columns: ColumnsType<BlacklistRecord> = [
|
||||
{
|
||||
title: 'Buyer Name',
|
||||
dataIndex: 'buyer_name',
|
||||
key: 'buyer_name',
|
||||
width: 150,
|
||||
render: (name: string, record: BlacklistRecord) => (
|
||||
<a onClick={() => handleViewDetail(record)}>{name}</a>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Email',
|
||||
dataIndex: 'buyer_email',
|
||||
key: 'buyer_email',
|
||||
width: 200,
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: 'Platform',
|
||||
dataIndex: 'platform',
|
||||
key: 'platform',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: 'Platform Buyer ID',
|
||||
dataIndex: 'platform_buyer_id',
|
||||
key: 'platform_buyer_id',
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
title: 'Type',
|
||||
dataIndex: 'blacklist_type',
|
||||
key: 'blacklist_type',
|
||||
width: 100,
|
||||
render: (type: string) => {
|
||||
const config = BLACKLIST_TYPES.find(t => t.value === type);
|
||||
return <Tag color={config?.color}>{type}</Tag>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Risk Score',
|
||||
dataIndex: 'risk_score',
|
||||
key: 'risk_score',
|
||||
width: 120,
|
||||
render: (score: number) => {
|
||||
const { level, color } = getRiskLevel(score);
|
||||
return (
|
||||
<Tag color={color}>
|
||||
{score} - {level}
|
||||
</Tag>
|
||||
);
|
||||
},
|
||||
sorter: (a, b) => a.risk_score - b.risk_score,
|
||||
},
|
||||
{
|
||||
title: 'Blacklist Date',
|
||||
dataIndex: 'blacklist_date',
|
||||
key: 'blacklist_date',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: 'Expiry Date',
|
||||
dataIndex: 'expiry_date',
|
||||
key: 'expiry_date',
|
||||
width: 120,
|
||||
render: (date: string) => {
|
||||
if (!date) return '-';
|
||||
const daysUntilExpiry = dayjs(date).diff(dayjs(), 'day');
|
||||
const isExpiringSoon = daysUntilExpiry > 0 && daysUntilExpiry <= 30;
|
||||
return (
|
||||
<Tooltip title={isExpiringSoon ? `Expires in ${daysUntilExpiry} days` : ''}>
|
||||
<span style={{ color: isExpiringSoon || daysUntilExpiry <= 0 ? '#ff4d4f' : undefined }}>
|
||||
{date}
|
||||
</span>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Status',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
width: 100,
|
||||
render: (status: string) => {
|
||||
const config = STATUS_CONFIG[status];
|
||||
return <Tag color={config.color} icon={config.icon}>{config.text}</Tag>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Actions',
|
||||
key: 'actions',
|
||||
width: 150,
|
||||
render: (_, record: BlacklistRecord) => (
|
||||
<Space>
|
||||
<Tooltip title="View Details">
|
||||
<Button type="link" icon={<EyeOutlined />} onClick={() => handleViewDetail(record)} />
|
||||
</Tooltip>
|
||||
<Tooltip title="Edit">
|
||||
<Button type="link" icon={<EditOutlined />} onClick={() => handleEditClick(record)} />
|
||||
</Tooltip>
|
||||
{record.status === 'ACTIVE' && (
|
||||
<Tooltip title="Remove">
|
||||
<Button type="link" danger icon={<DeleteOutlined />} onClick={() => handleDelete(record.id)} />
|
||||
</Tooltip>
|
||||
)}
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const stats = {
|
||||
total: blacklists.length,
|
||||
active: blacklists.filter(bl => bl.status === 'ACTIVE').length,
|
||||
inactive: blacklists.filter(bl => bl.status === 'INACTIVE').length,
|
||||
expired: blacklists.filter(bl => bl.status === 'EXPIRED').length,
|
||||
highRisk: blacklists.filter(bl => bl.risk_score >= 60).length,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="blacklist-manage-page">
|
||||
<Row gutter={16} style={{ marginBottom: 24 }}>
|
||||
<Col span={4}>
|
||||
<Card>
|
||||
<Statistic title="Total Blacklisted" value={stats.total} prefix={<UserOutlined />} />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<Card>
|
||||
<Statistic title="Active" value={stats.active} valueStyle={{ color: '#ff4d4f' }} prefix={<StopOutlined />} />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<Card>
|
||||
<Statistic title="Inactive" value={stats.inactive} valueStyle={{ color: '#8c8c8c' }} prefix={<CloseCircleOutlined />} />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<Card>
|
||||
<Statistic title="High Risk" value={stats.highRisk} valueStyle={{ color: '#ff7a45' }} prefix={<WarningOutlined />} />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Card>
|
||||
<Alert
|
||||
message="Blacklist Alert"
|
||||
description={`${stats.active} buyers are currently blacklisted. ${stats.highRisk} are high risk.`}
|
||||
type="error"
|
||||
showIcon
|
||||
icon={<WarningOutlined />}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Card title="Blacklist Management" extra={
|
||||
<Space>
|
||||
{selectedRowKeys.length > 0 && (
|
||||
<Button danger icon={<DeleteOutlined />} onClick={handleBatchDelete}>
|
||||
Batch Remove ({selectedRowKeys.length})
|
||||
</Button>
|
||||
)}
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={() => setCreateModalVisible(true)}>
|
||||
Add to Blacklist
|
||||
</Button>
|
||||
</Space>
|
||||
}>
|
||||
<div style={{ display: 'flex', marginBottom: 16, gap: 16 }}>
|
||||
<Input
|
||||
placeholder="Search by name, email, or platform buyer ID"
|
||||
prefix={<SearchOutlined />}
|
||||
style={{ width: 300 }}
|
||||
onChange={(e) => setSearchText(e.target.value)}
|
||||
/>
|
||||
<Select
|
||||
placeholder="Filter by Platform"
|
||||
style={{ width: 150 }}
|
||||
onChange={setPlatformFilter}
|
||||
allowClear
|
||||
>
|
||||
{PLATFORMS.map(platform => (
|
||||
<Option key={platform} value={platform}>{platform}</Option>
|
||||
))}
|
||||
</Select>
|
||||
<Select
|
||||
placeholder="Filter by Type"
|
||||
style={{ width: 150 }}
|
||||
onChange={setTypeFilter}
|
||||
allowClear
|
||||
>
|
||||
{BLACKLIST_TYPES.map(type => (
|
||||
<Option key={type.value} value={type.value}>{type.label}</Option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<Tabs activeKey={activeTab} onChange={setActiveTab}>
|
||||
<TabPane tab="All" key="all" />
|
||||
<TabPane tab="Active" key="active" />
|
||||
<TabPane tab="Inactive" key="inactive" />
|
||||
<TabPane tab="Expired" key="expired" />
|
||||
</Tabs>
|
||||
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={getFilteredBlacklists()}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
rowSelection={{
|
||||
selectedRowKeys,
|
||||
onChange: setSelectedRowKeys,
|
||||
getCheckboxProps: (record) => ({
|
||||
disabled: record.status !== 'ACTIVE',
|
||||
}),
|
||||
}}
|
||||
pagination={{ pageSize: 10 }}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Modal
|
||||
title="Add to Blacklist"
|
||||
open={createModalVisible}
|
||||
onCancel={() => setCreateModalVisible(false)}
|
||||
footer={null}
|
||||
width={700}
|
||||
>
|
||||
<Form form={form} layout="vertical" onFinish={handleCreate}>
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Form.Item name="platform" label="Platform" rules={[{ required: true }]}>
|
||||
<Select placeholder="Select Platform">
|
||||
{PLATFORMS.map(platform => (
|
||||
<Option key={platform} value={platform}>{platform}</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item name="platform_buyer_id" label="Platform Buyer ID" rules={[{ required: true }]}>
|
||||
<Input placeholder="Enter platform buyer ID" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Form.Item name="buyer_name" label="Buyer Name" rules={[{ required: true }]}>
|
||||
<Input placeholder="Enter buyer name" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item name="buyer_email" label="Buyer Email" rules={[{ required: true, type: 'email' }]}>
|
||||
<Input placeholder="Enter buyer email" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Form.Item name="buyer_phone" label="Buyer Phone">
|
||||
<Input placeholder="Enter buyer phone" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item name="blacklist_type" label="Blacklist Type" rules={[{ required: true }]}>
|
||||
<Select placeholder="Select Type">
|
||||
{BLACKLIST_TYPES.map(type => (
|
||||
<Option key={type.value} value={type.value}>{type.label}</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Form.Item name="blacklist_reason" label="Reason" rules={[{ required: true }]}>
|
||||
<TextArea rows={3} placeholder="Enter reason for blacklisting" />
|
||||
</Form.Item>
|
||||
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Form.Item name="risk_score" label="Risk Score" rules={[{ required: true }]}>
|
||||
<InputNumber min={0} max={100} style={{ width: '100%' }} placeholder="0-100" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item name="expiry_date" label="Expiry Date">
|
||||
<DatePicker style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Form.Item name="evidence" label="Evidence">
|
||||
<Upload>
|
||||
<Button icon={<UploadOutlined />}>Upload Evidence</Button>
|
||||
</Upload>
|
||||
</Form.Item>
|
||||
|
||||
<div style={{ textAlign: 'right' }}>
|
||||
<Button onClick={() => setCreateModalVisible(false)} style={{ marginRight: 8 }}>Cancel</Button>
|
||||
<Button type="primary" htmlType="submit" loading={loading}>Add to Blacklist</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
title="Edit Blacklist Record"
|
||||
open={editModalVisible}
|
||||
onCancel={() => {
|
||||
setEditModalVisible(false);
|
||||
setSelectedBlacklist(null);
|
||||
form.resetFields();
|
||||
}}
|
||||
footer={null}
|
||||
width={700}
|
||||
>
|
||||
<Form form={form} layout="vertical" onFinish={handleEdit}>
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Form.Item name="platform" label="Platform" rules={[{ required: true }]}>
|
||||
<Select placeholder="Select Platform">
|
||||
{PLATFORMS.map(platform => (
|
||||
<Option key={platform} value={platform}>{platform}</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item name="platform_buyer_id" label="Platform Buyer ID" rules={[{ required: true }]}>
|
||||
<Input placeholder="Enter platform buyer ID" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Form.Item name="buyer_name" label="Buyer Name" rules={[{ required: true }]}>
|
||||
<Input placeholder="Enter buyer name" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item name="buyer_email" label="Buyer Email" rules={[{ required: true, type: 'email' }]}>
|
||||
<Input placeholder="Enter buyer email" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Form.Item name="buyer_phone" label="Buyer Phone">
|
||||
<Input placeholder="Enter buyer phone" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item name="blacklist_type" label="Blacklist Type" rules={[{ required: true }]}>
|
||||
<Select placeholder="Select Type">
|
||||
{BLACKLIST_TYPES.map(type => (
|
||||
<Option key={type.value} value={type.value}>{type.label}</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Form.Item name="blacklist_reason" label="Reason" rules={[{ required: true }]}>
|
||||
<TextArea rows={3} placeholder="Enter reason for blacklisting" />
|
||||
</Form.Item>
|
||||
|
||||
<Row gutter={16}>
|
||||
<Col span={8}>
|
||||
<Form.Item name="risk_score" label="Risk Score" rules={[{ required: true }]}>
|
||||
<InputNumber min={0} max={100} style={{ width: '100%' }} placeholder="0-100" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Form.Item name="expiry_date" label="Expiry Date">
|
||||
<DatePicker style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Form.Item name="status" label="Status" rules={[{ required: true }]}>
|
||||
<Select placeholder="Select Status">
|
||||
<Option value="ACTIVE">Active</Option>
|
||||
<Option value="INACTIVE">Inactive</Option>
|
||||
<Option value="EXPIRED">Expired</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Form.Item name="evidence" label="Evidence">
|
||||
<Upload>
|
||||
<Button icon={<UploadOutlined />}>Upload Evidence</Button>
|
||||
</Upload>
|
||||
</Form.Item>
|
||||
|
||||
<div style={{ textAlign: 'right' }}>
|
||||
<Button onClick={() => {
|
||||
setEditModalVisible(false);
|
||||
setSelectedBlacklist(null);
|
||||
form.resetFields();
|
||||
}} style={{ marginRight: 8 }}>Cancel</Button>
|
||||
<Button type="primary" htmlType="submit" loading={loading}>Update Record</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
title="Blacklist Record Details"
|
||||
open={detailModalVisible}
|
||||
onCancel={() => setDetailModalVisible(false)}
|
||||
footer={[
|
||||
<Button key="close" onClick={() => setDetailModalVisible(false)}>Close</Button>,
|
||||
selectedBlacklist?.status === 'ACTIVE' && (
|
||||
<Button key="edit" type="primary" icon={<EditOutlined />} onClick={() => {
|
||||
setDetailModalVisible(false);
|
||||
handleEditClick(selectedBlacklist!);
|
||||
}}>
|
||||
Edit Record
|
||||
</Button>
|
||||
),
|
||||
]}
|
||||
width={700}
|
||||
>
|
||||
{selectedBlacklist && (
|
||||
<>
|
||||
<Descriptions bordered column={2}>
|
||||
<Descriptions.Item label="Buyer Name" span={2}>{selectedBlacklist.buyer_name}</Descriptions.Item>
|
||||
<Descriptions.Item label="Email">{selectedBlacklist.buyer_email}</Descriptions.Item>
|
||||
<Descriptions.Item label="Phone">{selectedBlacklist.buyer_phone || '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label="Platform">{selectedBlacklist.platform}</Descriptions.Item>
|
||||
<Descriptions.Item label="Platform Buyer ID">{selectedBlacklist.platform_buyer_id}</Descriptions.Item>
|
||||
<Descriptions.Item label="Blacklist Type">
|
||||
<Tag color={BLACKLIST_TYPES.find(t => t.value === selectedBlacklist.blacklist_type)?.color}>
|
||||
{BLACKLIST_TYPES.find(t => t.value === selectedBlacklist.blacklist_type)?.label}
|
||||
</Tag>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="Risk Score">
|
||||
<Tag color={getRiskLevel(selectedBlacklist.risk_score).color}>
|
||||
{selectedBlacklist.risk_score} - {getRiskLevel(selectedBlacklist.risk_score).level}
|
||||
</Tag>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="Status">
|
||||
<Tag color={STATUS_CONFIG[selectedBlacklist.status].color} icon={STATUS_CONFIG[selectedBlacklist.status].icon}>
|
||||
{STATUS_CONFIG[selectedBlacklist.status].text}
|
||||
</Tag>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="Blacklist Date">{selectedBlacklist.blacklist_date}</Descriptions.Item>
|
||||
<Descriptions.Item label="Expiry Date">{selectedBlacklist.expiry_date || '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label="Reason" span={2}>{selectedBlacklist.blacklist_reason}</Descriptions.Item>
|
||||
<Descriptions.Item label="Evidence" span={2}>{selectedBlacklist.evidence || '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label="Created By">{selectedBlacklist.created_by}</Descriptions.Item>
|
||||
<Descriptions.Item label="Created At">{selectedBlacklist.created_at}</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</>
|
||||
)}
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BlacklistManage;
|
||||
705
dashboard/src/pages/Blacklist/RiskMonitor.tsx
Normal file
705
dashboard/src/pages/Blacklist/RiskMonitor.tsx
Normal file
@@ -0,0 +1,705 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Card,
|
||||
Table,
|
||||
Button,
|
||||
Space,
|
||||
Modal,
|
||||
Form,
|
||||
Select,
|
||||
DatePicker,
|
||||
Descriptions,
|
||||
Divider,
|
||||
message,
|
||||
Tag,
|
||||
Tabs,
|
||||
Row,
|
||||
Col,
|
||||
Statistic,
|
||||
Alert,
|
||||
Tooltip,
|
||||
Progress,
|
||||
Badge,
|
||||
List,
|
||||
Empty,
|
||||
} from 'antd';
|
||||
import {
|
||||
EyeOutlined,
|
||||
SearchOutlined,
|
||||
WarningOutlined,
|
||||
CheckCircleOutlined,
|
||||
CloseCircleOutlined,
|
||||
ExclamationCircleOutlined,
|
||||
ThunderboltOutlined,
|
||||
SafetyOutlined,
|
||||
RadarChartOutlined,
|
||||
LineChartOutlined,
|
||||
BarChartOutlined,
|
||||
DashboardOutlined,
|
||||
FilterOutlined,
|
||||
ReloadOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
const { Option } = Select;
|
||||
const { TabPane } = Tabs;
|
||||
const { RangePicker } = DatePicker;
|
||||
|
||||
interface RiskAssessment {
|
||||
id: string;
|
||||
tenant_id: string;
|
||||
shop_id: string;
|
||||
buyer_id: string;
|
||||
platform: string;
|
||||
platform_buyer_id: string;
|
||||
risk_score: number;
|
||||
risk_level: 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL';
|
||||
risk_factors: string[];
|
||||
recommendations: string[];
|
||||
is_blacklisted: boolean;
|
||||
blacklist_reasons?: string[];
|
||||
confidence_score: number;
|
||||
assessment_reason: string;
|
||||
assessment_date: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
interface RiskStatistics {
|
||||
total_assessments: number;
|
||||
high_risk_count: number;
|
||||
medium_risk_count: number;
|
||||
low_risk_count: number;
|
||||
critical_risk_count: number;
|
||||
blacklist_conversion_rate: number;
|
||||
false_positives: number;
|
||||
false_negatives: number;
|
||||
by_platform: Record<string, number>;
|
||||
by_risk_level: Record<string, number>;
|
||||
average_risk_score: number;
|
||||
risk_trend: {
|
||||
date: string;
|
||||
average_score: number;
|
||||
}[];
|
||||
}
|
||||
|
||||
const RISK_LEVEL_CONFIG: Record<string, { color: string; text: string; icon: React.ReactNode; percentage: number }> = {
|
||||
LOW: { color: 'success', text: 'Low', icon: <CheckCircleOutlined />, percentage: 25 },
|
||||
MEDIUM: { color: 'warning', text: 'Medium', icon: <ExclamationCircleOutlined />, percentage: 50 },
|
||||
HIGH: { color: 'error', text: 'High', icon: <WarningOutlined />, percentage: 75 },
|
||||
CRITICAL: { color: 'red', text: 'Critical', icon: <CloseCircleOutlined />, percentage: 100 },
|
||||
};
|
||||
|
||||
const PLATFORMS = [
|
||||
'Amazon',
|
||||
'eBay',
|
||||
'Shopify',
|
||||
'Walmart',
|
||||
'TikTok Shop',
|
||||
'Temu',
|
||||
'Alibaba',
|
||||
'Other',
|
||||
];
|
||||
|
||||
const MOCK_ASSESSMENTS: RiskAssessment[] = [
|
||||
{
|
||||
id: '1',
|
||||
tenant_id: 'tenant-001',
|
||||
shop_id: 'shop-001',
|
||||
buyer_id: 'buyer-001',
|
||||
platform: 'Amazon',
|
||||
platform_buyer_id: 'AMZ-001',
|
||||
risk_score: 85,
|
||||
risk_level: 'CRITICAL',
|
||||
risk_factors: ['High return rate', 'Multiple chargebacks', 'Suspicious activity'],
|
||||
recommendations: ['Block buyer immediately', 'Add to blacklist', 'Review all past transactions'],
|
||||
is_blacklisted: true,
|
||||
blacklist_reasons: ['Fraudulent transactions'],
|
||||
confidence_score: 85,
|
||||
assessment_reason: 'New order received',
|
||||
assessment_date: '2026-03-18 10:30:00',
|
||||
created_at: '2026-03-18 10:30:00',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
tenant_id: 'tenant-001',
|
||||
shop_id: 'shop-001',
|
||||
buyer_id: 'buyer-002',
|
||||
platform: 'eBay',
|
||||
platform_buyer_id: 'EBAY-002',
|
||||
risk_score: 72,
|
||||
risk_level: 'HIGH',
|
||||
risk_factors: ['High return rate', 'New account'],
|
||||
recommendations: ['Place order on hold', 'Request additional verification', 'Limit order amount'],
|
||||
is_blacklisted: false,
|
||||
confidence_score: 78,
|
||||
assessment_reason: 'New order received',
|
||||
assessment_date: '2026-03-18 11:15:00',
|
||||
created_at: '2026-03-18 11:15:00',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
tenant_id: 'tenant-001',
|
||||
shop_id: 'shop-001',
|
||||
buyer_id: 'buyer-003',
|
||||
platform: 'Amazon',
|
||||
platform_buyer_id: 'AMZ-003',
|
||||
risk_score: 45,
|
||||
risk_level: 'MEDIUM',
|
||||
risk_factors: ['Moderate return rate'],
|
||||
recommendations: ['Monitor transaction closely', 'Set lower order limits'],
|
||||
is_blacklisted: false,
|
||||
confidence_score: 65,
|
||||
assessment_reason: 'New order received',
|
||||
assessment_date: '2026-03-18 12:00:00',
|
||||
created_at: '2026-03-18 12:00:00',
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
tenant_id: 'tenant-001',
|
||||
shop_id: 'shop-001',
|
||||
buyer_id: 'buyer-004',
|
||||
platform: 'Shopify',
|
||||
platform_buyer_id: 'SHOPIFY-004',
|
||||
risk_score: 15,
|
||||
risk_level: 'LOW',
|
||||
risk_factors: [],
|
||||
recommendations: ['Process order normally', 'Continue monitoring'],
|
||||
is_blacklisted: false,
|
||||
confidence_score: 90,
|
||||
assessment_reason: 'New order received',
|
||||
assessment_date: '2026-03-18 12:30:00',
|
||||
created_at: '2026-03-18 12:30:00',
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
tenant_id: 'tenant-001',
|
||||
shop_id: 'shop-001',
|
||||
buyer_id: 'buyer-005',
|
||||
platform: 'Walmart',
|
||||
platform_buyer_id: 'WMT-005',
|
||||
risk_score: 65,
|
||||
risk_level: 'HIGH',
|
||||
risk_factors: ['Chargeback history', 'Multiple IP changes'],
|
||||
recommendations: ['Place order on hold', 'Use secure payment methods', 'Monitor future transactions'],
|
||||
is_blacklisted: true,
|
||||
blacklist_reasons: ['Excessive chargebacks'],
|
||||
confidence_score: 72,
|
||||
assessment_reason: 'New order received',
|
||||
assessment_date: '2026-03-18 13:00:00',
|
||||
created_at: '2026-03-18 13:00:00',
|
||||
},
|
||||
];
|
||||
|
||||
const MOCK_STATISTICS: RiskStatistics = {
|
||||
total_assessments: 150,
|
||||
high_risk_count: 35,
|
||||
medium_risk_count: 60,
|
||||
low_risk_count: 45,
|
||||
critical_risk_count: 10,
|
||||
blacklist_conversion_rate: 12.5,
|
||||
false_positives: 3,
|
||||
false_negatives: 1,
|
||||
by_platform: {
|
||||
'Amazon': 60,
|
||||
'eBay': 35,
|
||||
'Shopify': 25,
|
||||
'Walmart': 15,
|
||||
'TikTok Shop': 10,
|
||||
'Temu': 5,
|
||||
},
|
||||
by_risk_level: {
|
||||
'LOW': 45,
|
||||
'MEDIUM': 60,
|
||||
'HIGH': 35,
|
||||
'CRITICAL': 10,
|
||||
},
|
||||
average_risk_score: 42.5,
|
||||
risk_trend: [
|
||||
{ date: '2026-03-12', average_score: 38.2 },
|
||||
{ date: '2026-03-13', average_score: 40.5 },
|
||||
{ date: '2026-03-14', average_score: 39.8 },
|
||||
{ date: '2026-03-15', average_score: 41.2 },
|
||||
{ date: '2026-03-16', average_score: 43.5 },
|
||||
{ date: '2026-03-17', average_score: 42.1 },
|
||||
{ date: '2026-03-18', average_score: 42.5 },
|
||||
],
|
||||
};
|
||||
|
||||
const RiskMonitor: React.FC = () => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [assessments, setAssessments] = useState<RiskAssessment[]>(MOCK_ASSESSMENTS);
|
||||
const [statistics, setStatistics] = useState<RiskStatistics>(MOCK_STATISTICS);
|
||||
const [detailModalVisible, setDetailModalVisible] = useState(false);
|
||||
const [selectedAssessment, setSelectedAssessment] = useState<RiskAssessment | null>(null);
|
||||
const [activeTab, setActiveTab] = useState('all');
|
||||
const [platformFilter, setPlatformFilter] = useState<string>('');
|
||||
const [riskLevelFilter, setRiskLevelFilter] = useState<string>('');
|
||||
const [dateRange, setDateRange] = useState<[dayjs.Dayjs, dayjs.Dayjs] | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetchRiskData();
|
||||
}, []);
|
||||
|
||||
const fetchRiskData = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
setAssessments(MOCK_ASSESSMENTS);
|
||||
setStatistics(MOCK_STATISTICS);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleViewDetail = (assessment: RiskAssessment) => {
|
||||
setSelectedAssessment(assessment);
|
||||
setDetailModalVisible(true);
|
||||
};
|
||||
|
||||
const getFilteredAssessments = () => {
|
||||
let filtered = assessments;
|
||||
|
||||
if (activeTab !== 'all') {
|
||||
filtered = filtered.filter(a => a.risk_level === activeTab.toUpperCase());
|
||||
}
|
||||
|
||||
if (platformFilter) {
|
||||
filtered = filtered.filter(a => a.platform === platformFilter);
|
||||
}
|
||||
|
||||
if (riskLevelFilter) {
|
||||
filtered = filtered.filter(a => a.risk_level === riskLevelFilter.toUpperCase());
|
||||
}
|
||||
|
||||
if (dateRange) {
|
||||
filtered = filtered.filter(a => {
|
||||
const assessmentDate = dayjs(a.assessment_date);
|
||||
return assessmentDate.isAfter(dateRange[0].startOf('day')) &&
|
||||
assessmentDate.isBefore(dateRange[1].endOf('day'));
|
||||
});
|
||||
}
|
||||
|
||||
return filtered;
|
||||
};
|
||||
|
||||
const getRiskColor = (score: number): string => {
|
||||
if (score >= 80) return '#ff4d4f';
|
||||
if (score >= 60) return '#ff7a45';
|
||||
if (score >= 40) return '#faad14';
|
||||
return '#52c41a';
|
||||
};
|
||||
|
||||
const columns: ColumnsType<RiskAssessment> = [
|
||||
{
|
||||
title: 'Platform',
|
||||
dataIndex: 'platform',
|
||||
key: 'platform',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: 'Platform Buyer ID',
|
||||
dataIndex: 'platform_buyer_id',
|
||||
key: 'platform_buyer_id',
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
title: 'Risk Score',
|
||||
dataIndex: 'risk_score',
|
||||
key: 'risk_score',
|
||||
width: 150,
|
||||
render: (score: number, record: RiskAssessment) => (
|
||||
<div>
|
||||
<Progress
|
||||
percent={score}
|
||||
strokeColor={getRiskColor(score)}
|
||||
size="small"
|
||||
format={percent => `${percent}`}
|
||||
/>
|
||||
<Tag color={getRiskColor(score)} style={{ marginTop: 4 }}>
|
||||
{record.risk_level}
|
||||
</Tag>
|
||||
</div>
|
||||
),
|
||||
sorter: (a, b) => a.risk_score - b.risk_score,
|
||||
},
|
||||
{
|
||||
title: 'Risk Factors',
|
||||
dataIndex: 'risk_factors',
|
||||
key: 'risk_factors',
|
||||
width: 200,
|
||||
render: (factors: string[]) => (
|
||||
<div>
|
||||
{factors.slice(0, 2).map((factor, index) => (
|
||||
<Tag key={index} color="orange" style={{ marginBottom: 2 }}>
|
||||
{factor}
|
||||
</Tag>
|
||||
))}
|
||||
{factors.length > 2 && (
|
||||
<Tooltip title={factors.slice(2).join(', ')}>
|
||||
<Tag color="default">+{factors.length - 2} more</Tag>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Blacklisted',
|
||||
dataIndex: 'is_blacklisted',
|
||||
key: 'is_blacklisted',
|
||||
width: 100,
|
||||
render: (isBlacklisted: boolean) => (
|
||||
<Tag color={isBlacklisted ? 'error' : 'success'} icon={isBlacklisted ? <CloseCircleOutlined /> : <CheckCircleOutlined />}>
|
||||
{isBlacklisted ? 'Yes' : 'No'}
|
||||
</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Confidence',
|
||||
dataIndex: 'confidence_score',
|
||||
key: 'confidence_score',
|
||||
width: 100,
|
||||
render: (score: number) => (
|
||||
<Progress
|
||||
percent={score}
|
||||
strokeColor="#1890ff"
|
||||
size="small"
|
||||
format={percent => `${percent}%`}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Assessment Date',
|
||||
dataIndex: 'assessment_date',
|
||||
key: 'assessment_date',
|
||||
width: 150,
|
||||
render: (date: string) => dayjs(date).format('YYYY-MM-DD HH:mm'),
|
||||
},
|
||||
{
|
||||
title: 'Actions',
|
||||
key: 'actions',
|
||||
width: 80,
|
||||
render: (_, record: RiskAssessment) => (
|
||||
<Tooltip title="View Details">
|
||||
<Button type="link" icon={<EyeOutlined />} onClick={() => handleViewDetail(record)} />
|
||||
</Tooltip>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const highRiskAlerts = assessments.filter(a => a.risk_level === 'HIGH' || a.risk_level === 'CRITICAL');
|
||||
|
||||
return (
|
||||
<div className="risk-monitor-page">
|
||||
<Row gutter={16} style={{ marginBottom: 24 }}>
|
||||
<Col span={4}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="Total Assessments"
|
||||
value={statistics.total_assessments}
|
||||
prefix={<DashboardOutlined />}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="Critical Risk"
|
||||
value={statistics.critical_risk_count}
|
||||
valueStyle={{ color: '#ff4d4f' }}
|
||||
prefix={<CloseCircleOutlined />}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="High Risk"
|
||||
value={statistics.high_risk_count}
|
||||
valueStyle={{ color: '#ff7a45' }}
|
||||
prefix={<WarningOutlined />}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="Medium Risk"
|
||||
value={statistics.medium_risk_count}
|
||||
valueStyle={{ color: '#faad14' }}
|
||||
prefix={<ExclamationCircleOutlined />}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="Low Risk"
|
||||
value={statistics.low_risk_count}
|
||||
valueStyle={{ color: '#52c41a' }}
|
||||
prefix={<CheckCircleOutlined />}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="Avg Risk Score"
|
||||
value={statistics.average_risk_score}
|
||||
precision={1}
|
||||
valueStyle={{ color: '#1890ff' }}
|
||||
prefix={<RadarChartOutlined />}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row gutter={16} style={{ marginBottom: 24 }}>
|
||||
<Col span={12}>
|
||||
<Card title="Risk Distribution" extra={<BarChartOutlined />}>
|
||||
<div style={{ marginTop: 16 }}>
|
||||
{Object.entries(statistics.by_risk_level).map(([level, count]) => {
|
||||
const config = RISK_LEVEL_CONFIG[level];
|
||||
const percentage = statistics.total_assessments > 0
|
||||
? (count / statistics.total_assessments) * 100
|
||||
: 0;
|
||||
return (
|
||||
<div key={level} style={{ marginBottom: 16 }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 4 }}>
|
||||
<span>
|
||||
<Tag color={config.color} icon={config.icon}>{config.text}</Tag>
|
||||
<span style={{ marginLeft: 8 }}>{count} assessments</span>
|
||||
</span>
|
||||
<span>{percentage.toFixed(1)}%</span>
|
||||
</div>
|
||||
<Progress
|
||||
percent={percentage}
|
||||
strokeColor={config.color}
|
||||
showInfo={false}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Card title="Risk by Platform" extra={<ThunderboltOutlined />}>
|
||||
<List
|
||||
dataSource={Object.entries(statistics.by_platform)
|
||||
.sort(([, a], [, b]) => b - a)
|
||||
.slice(0, 6)}
|
||||
renderItem={([platform, count]) => (
|
||||
<List.Item key={platform}>
|
||||
<List.Item.Meta
|
||||
title={platform}
|
||||
description={`${count} assessments`}
|
||||
/>
|
||||
<Progress
|
||||
percent={statistics.total_assessments > 0 ? (count / statistics.total_assessments) * 100 : 0}
|
||||
strokeColor="#1890ff"
|
||||
style={{ width: 150 }}
|
||||
showInfo={false}
|
||||
/>
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
{highRiskAlerts.length > 0 && (
|
||||
<Card
|
||||
title={
|
||||
<Space>
|
||||
<WarningOutlined style={{ color: '#ff4d4f' }} />
|
||||
<span>High Risk Alerts</span>
|
||||
<Badge count={highRiskAlerts.length} style={{ backgroundColor: '#ff4d4f' }} />
|
||||
</Space>
|
||||
}
|
||||
style={{ marginBottom: 24 }}
|
||||
>
|
||||
<Alert
|
||||
message={`${highRiskAlerts.length} buyers identified as high or critical risk`}
|
||||
description="Immediate action recommended for critical risk buyers"
|
||||
type="error"
|
||||
showIcon
|
||||
style={{ marginBottom: 16 }}
|
||||
/>
|
||||
<List
|
||||
dataSource={highRiskAlerts.slice(0, 5)}
|
||||
renderItem={(assessment) => (
|
||||
<List.Item
|
||||
actions={[
|
||||
<Button type="link" icon={<EyeOutlined />} onClick={() => handleViewDetail(assessment)}>
|
||||
View Details
|
||||
</Button>
|
||||
]}
|
||||
>
|
||||
<List.Item.Meta
|
||||
title={
|
||||
<Space>
|
||||
<span>{assessment.platform}</span>
|
||||
<span>{assessment.platform_buyer_id}</span>
|
||||
<Tag color={getRiskColor(assessment.risk_score)}>{assessment.risk_level}</Tag>
|
||||
</Space>
|
||||
}
|
||||
description={`Risk Score: ${assessment.risk_score} | ${dayjs(assessment.assessment_date).format('YYYY-MM-DD HH:mm')}`}
|
||||
/>
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
<Card
|
||||
title="Risk Assessment History"
|
||||
extra={
|
||||
<Space>
|
||||
<Button icon={<ReloadOutlined />} onClick={fetchRiskData}>Refresh</Button>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<div style={{ display: 'flex', marginBottom: 16, gap: 16, flexWrap: 'wrap' }}>
|
||||
<Select
|
||||
placeholder="Filter by Platform"
|
||||
style={{ width: 150 }}
|
||||
onChange={setPlatformFilter}
|
||||
allowClear
|
||||
suffixIcon={<FilterOutlined />}
|
||||
>
|
||||
{PLATFORMS.map(platform => (
|
||||
<Option key={platform} value={platform}>{platform}</Option>
|
||||
))}
|
||||
</Select>
|
||||
<Select
|
||||
placeholder="Filter by Risk Level"
|
||||
style={{ width: 150 }}
|
||||
onChange={setRiskLevelFilter}
|
||||
allowClear
|
||||
suffixIcon={<FilterOutlined />}
|
||||
>
|
||||
{Object.entries(RISK_LEVEL_CONFIG).map(([level, config]) => (
|
||||
<Option key={level} value={level}>
|
||||
<Tag color={config.color} icon={config.icon}>{config.text}</Tag>
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
<RangePicker
|
||||
style={{ width: 300 }}
|
||||
onChange={(dates) => setDateRange(dates as [dayjs.Dayjs, dayjs.Dayjs] | null)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Tabs activeKey={activeTab} onChange={setActiveTab}>
|
||||
<TabPane tab="All" key="all" />
|
||||
<TabPane tab={<Badge count={statistics.critical_risk_count} offset={[10, 0]} size="small">Critical</Badge>} key="critical" />
|
||||
<TabPane tab={<Badge count={statistics.high_risk_count} offset={[10, 0]} size="small">High</Badge>} key="high" />
|
||||
<TabPane tab="Medium" key="medium" />
|
||||
<TabPane tab="Low" key="low" />
|
||||
</Tabs>
|
||||
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={getFilteredAssessments()}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
pagination={{ pageSize: 10 }}
|
||||
locale={{
|
||||
emptyText: (
|
||||
<Empty
|
||||
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
||||
description="No risk assessments found"
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Modal
|
||||
title="Risk Assessment Details"
|
||||
open={detailModalVisible}
|
||||
onCancel={() => setDetailModalVisible(false)}
|
||||
footer={[
|
||||
<Button key="close" onClick={() => setDetailModalVisible(false)}>Close</Button>,
|
||||
]}
|
||||
width={800}
|
||||
>
|
||||
{selectedAssessment && (
|
||||
<>
|
||||
<Descriptions bordered column={2}>
|
||||
<Descriptions.Item label="Platform">{selectedAssessment.platform}</Descriptions.Item>
|
||||
<Descriptions.Item label="Platform Buyer ID">{selectedAssessment.platform_buyer_id}</Descriptions.Item>
|
||||
<Descriptions.Item label="Risk Score">
|
||||
<Progress
|
||||
percent={selectedAssessment.risk_score}
|
||||
strokeColor={getRiskColor(selectedAssessment.risk_score)}
|
||||
format={percent => `${percent}`}
|
||||
/>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="Risk Level">
|
||||
<Tag color={getRiskColor(selectedAssessment.risk_score)} icon={RISK_LEVEL_CONFIG[selectedAssessment.risk_level].icon}>
|
||||
{RISK_LEVEL_CONFIG[selectedAssessment.risk_level].text}
|
||||
</Tag>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="Confidence Score">
|
||||
<Progress
|
||||
percent={selectedAssessment.confidence_score}
|
||||
strokeColor="#1890ff"
|
||||
format={percent => `${percent}%`}
|
||||
/>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="Blacklisted">
|
||||
<Tag color={selectedAssessment.is_blacklisted ? 'error' : 'success'} icon={selectedAssessment.is_blacklisted ? <CloseCircleOutlined /> : <CheckCircleOutlined />}>
|
||||
{selectedAssessment.is_blacklisted ? 'Yes' : 'No'}
|
||||
</Tag>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="Assessment Reason" span={2}>{selectedAssessment.assessment_reason}</Descriptions.Item>
|
||||
<Descriptions.Item label="Assessment Date" span={2}>
|
||||
{dayjs(selectedAssessment.assessment_date).format('YYYY-MM-DD HH:mm:ss')}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
|
||||
<Divider>Risk Factors</Divider>
|
||||
{selectedAssessment.risk_factors.length > 0 ? (
|
||||
<Space wrap>
|
||||
{selectedAssessment.risk_factors.map((factor, index) => (
|
||||
<Tag key={index} color="orange">{factor}</Tag>
|
||||
))}
|
||||
</Space>
|
||||
) : (
|
||||
<Empty description="No risk factors identified" image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
||||
)}
|
||||
|
||||
<Divider>Recommendations</Divider>
|
||||
{selectedAssessment.recommendations.length > 0 ? (
|
||||
<List
|
||||
dataSource={selectedAssessment.recommendations}
|
||||
renderItem={(recommendation, index) => (
|
||||
<List.Item key={index}>
|
||||
<SafetyOutlined style={{ color: '#1890ff', marginRight: 8 }} />
|
||||
{recommendation}
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<Empty description="No recommendations" image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
||||
)}
|
||||
|
||||
{selectedAssessment.is_blacklisted && selectedAssessment.blacklist_reasons && (
|
||||
<>
|
||||
<Divider>Blacklist Reasons</Divider>
|
||||
<Space wrap>
|
||||
{selectedAssessment.blacklist_reasons.map((reason, index) => (
|
||||
<Tag key={index} color="error">{reason}</Tag>
|
||||
))}
|
||||
</Space>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default RiskMonitor;
|
||||
237
dashboard/src/pages/Merchant/MerchantManage.tsx
Normal file
237
dashboard/src/pages/Merchant/MerchantManage.tsx
Normal file
@@ -0,0 +1,237 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Table, Input, Button, Select, DatePicker, message, Card, Typography } from 'antd';
|
||||
import { SearchOutlined, PlusOutlined, EditOutlined, DeleteOutlined, EyeOutlined } from '@ant-design/icons';
|
||||
import { getMerchants, createMerchant, updateMerchant, deleteMerchant } from '../../services/merchantService';
|
||||
|
||||
const { Option } = Select;
|
||||
const { Title, Text } = Typography;
|
||||
|
||||
interface Merchant {
|
||||
id: string;
|
||||
tenantId: string;
|
||||
companyName: string;
|
||||
businessLicense: string;
|
||||
contactEmail: string;
|
||||
contactPhone: string;
|
||||
status: 'PENDING' | 'ACTIVE' | 'SUSPENDED' | 'TERMINATED';
|
||||
tier: 'BASIC' | 'PRO' | 'ENTERPRISE';
|
||||
traceId: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
const MerchantManage: React.FC = () => {
|
||||
const [merchants, setMerchants] = useState<Merchant[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [searchText, setSearchText] = useState('');
|
||||
const [statusFilter, setStatusFilter] = useState<string>('');
|
||||
const [tierFilter, setTierFilter] = useState<string>('');
|
||||
|
||||
const fetchMerchants = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await getMerchants();
|
||||
setMerchants(response.data);
|
||||
} catch (error) {
|
||||
message.error('获取商户列表失败');
|
||||
console.error('获取商户列表失败:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchMerchants();
|
||||
}, []);
|
||||
|
||||
const handleSearch = (value: string) => {
|
||||
setSearchText(value);
|
||||
};
|
||||
|
||||
const handleStatusFilter = (value: string) => {
|
||||
setStatusFilter(value);
|
||||
};
|
||||
|
||||
const handleTierFilter = (value: string) => {
|
||||
setTierFilter(value);
|
||||
};
|
||||
|
||||
const filteredMerchants = merchants.filter(merchant => {
|
||||
const matchesSearch = merchant.companyName.toLowerCase().includes(searchText.toLowerCase()) ||
|
||||
merchant.contactEmail.toLowerCase().includes(searchText.toLowerCase());
|
||||
const matchesStatus = statusFilter ? merchant.status === statusFilter : true;
|
||||
const matchesTier = tierFilter ? merchant.tier === tierFilter : true;
|
||||
return matchesSearch && matchesStatus && matchesTier;
|
||||
});
|
||||
|
||||
const handleCreate = () => {
|
||||
// 跳转到创建商户页面或打开创建弹窗
|
||||
message.info('创建商户功能待实现');
|
||||
};
|
||||
|
||||
const handleEdit = (merchant: Merchant) => {
|
||||
// 跳转到编辑商户页面或打开编辑弹窗
|
||||
message.info(`编辑商户: ${merchant.companyName}`);
|
||||
};
|
||||
|
||||
const handleDelete = (merchantId: string) => {
|
||||
// 确认删除后调用API
|
||||
message.info(`删除商户: ${merchantId}`);
|
||||
};
|
||||
|
||||
const handleView = (merchant: Merchant) => {
|
||||
// 跳转到商户详情页面
|
||||
message.info(`查看商户详情: ${merchant.companyName}`);
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '商户ID',
|
||||
dataIndex: 'id',
|
||||
key: 'id',
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: '公司名称',
|
||||
dataIndex: 'companyName',
|
||||
key: 'companyName',
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: '联系邮箱',
|
||||
dataIndex: 'contactEmail',
|
||||
key: 'contactEmail',
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: '联系电话',
|
||||
dataIndex: 'contactPhone',
|
||||
key: 'contactPhone',
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
render: (status: string) => {
|
||||
const statusMap: Record<string, string> = {
|
||||
PENDING: '待审核',
|
||||
ACTIVE: '活跃',
|
||||
SUSPENDED: '暂停',
|
||||
TERMINATED: '终止',
|
||||
};
|
||||
return <Text>{statusMap[status] || status}</Text>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '等级',
|
||||
dataIndex: 'tier',
|
||||
key: 'tier',
|
||||
render: (tier: string) => {
|
||||
const tierMap: Record<string, string> = {
|
||||
BASIC: '基础版',
|
||||
PRO: '专业版',
|
||||
ENTERPRISE: '企业版',
|
||||
};
|
||||
return <Text>{tierMap[tier] || tier}</Text>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'createdAt',
|
||||
key: 'createdAt',
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
render: (_: any, merchant: Merchant) => (
|
||||
<div>
|
||||
<Button
|
||||
icon={<EyeOutlined />}
|
||||
size="small"
|
||||
style={{ marginRight: 8 }}
|
||||
onClick={() => handleView(merchant)}
|
||||
>
|
||||
查看
|
||||
</Button>
|
||||
<Button
|
||||
icon={<EditOutlined />}
|
||||
size="small"
|
||||
style={{ marginRight: 8 }}
|
||||
onClick={() => handleEdit(merchant)}
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
<Button
|
||||
icon={<DeleteOutlined />}
|
||||
size="small"
|
||||
danger
|
||||
onClick={() => handleDelete(merchant.id)}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 24 }}>
|
||||
<Title level={4}>商户管理</Title>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={handleCreate}
|
||||
>
|
||||
新增商户
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', marginBottom: 16, gap: 16 }}>
|
||||
<Input
|
||||
placeholder="搜索商户名称或邮箱"
|
||||
prefix={<SearchOutlined />}
|
||||
style={{ width: 300 }}
|
||||
onChange={(e) => handleSearch(e.target.value)}
|
||||
/>
|
||||
<Select
|
||||
placeholder="筛选状态"
|
||||
style={{ width: 120 }}
|
||||
onChange={handleStatusFilter}
|
||||
allowClear
|
||||
>
|
||||
<Option value="PENDING">待审核</Option>
|
||||
<Option value="ACTIVE">活跃</Option>
|
||||
<Option value="SUSPENDED">暂停</Option>
|
||||
<Option value="TERMINATED">终止</Option>
|
||||
</Select>
|
||||
<Select
|
||||
placeholder="筛选等级"
|
||||
style={{ width: 120 }}
|
||||
onChange={handleTierFilter}
|
||||
allowClear
|
||||
>
|
||||
<Option value="BASIC">基础版</Option>
|
||||
<Option value="PRO">专业版</Option>
|
||||
<Option value="ENTERPRISE">企业版</Option>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={filteredMerchants}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
pagination={{
|
||||
showSizeChanger: true,
|
||||
pageSizeOptions: ['10', '20', '50'],
|
||||
defaultPageSize: 10,
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default MerchantManage;
|
||||
273
dashboard/src/pages/Merchant/MerchantOrderManage.tsx
Normal file
273
dashboard/src/pages/Merchant/MerchantOrderManage.tsx
Normal file
@@ -0,0 +1,273 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Table, Input, Button, Select, DatePicker, message, Card, Typography, Tag } from 'antd';
|
||||
import { SearchOutlined, EyeOutlined, ReloadOutlined } from '@ant-design/icons';
|
||||
import { getMerchantOrders, updateOrderStatus } from '../../services/merchantOrderService';
|
||||
|
||||
const { Option } = Select;
|
||||
const { Title, Text } = Typography;
|
||||
const { RangePicker } = DatePicker;
|
||||
|
||||
interface Order {
|
||||
id: string;
|
||||
merchantId: string;
|
||||
merchantName: string;
|
||||
orderId: string;
|
||||
platform: string;
|
||||
totalAmount: number;
|
||||
status: 'PENDING' | 'PROCESSING' | 'SHIPPED' | 'DELIVERED' | 'CANCELLED' | 'REFUNDED';
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
const MerchantOrderManage: React.FC = () => {
|
||||
const [orders, setOrders] = useState<Order[]>([]);
|
||||
const [merchants, setMerchants] = useState<{ id: string; companyName: string }[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [searchText, setSearchText] = useState('');
|
||||
const [merchantFilter, setMerchantFilter] = useState<string>('');
|
||||
const [platformFilter, setPlatformFilter] = useState<string>('');
|
||||
const [statusFilter, setStatusFilter] = useState<string>('');
|
||||
const [dateRange, setDateRange] = useState<[string, string] | null>(null);
|
||||
|
||||
const fetchOrders = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await getMerchantOrders();
|
||||
setOrders(response.data);
|
||||
} catch (error) {
|
||||
message.error('获取订单列表失败');
|
||||
console.error('获取订单列表失败:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchMerchants = async () => {
|
||||
try {
|
||||
// 实际项目中应该调用获取商户列表的API
|
||||
// 这里模拟数据
|
||||
setMerchants([
|
||||
{ id: '1', companyName: '商户A' },
|
||||
{ id: '2', companyName: '商户B' },
|
||||
{ id: '3', companyName: '商户C' },
|
||||
]);
|
||||
} catch (error) {
|
||||
console.error('获取商户列表失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchMerchants();
|
||||
fetchOrders();
|
||||
}, []);
|
||||
|
||||
const handleSearch = (value: string) => {
|
||||
setSearchText(value);
|
||||
};
|
||||
|
||||
const handleMerchantFilter = (value: string) => {
|
||||
setMerchantFilter(value);
|
||||
};
|
||||
|
||||
const handlePlatformFilter = (value: string) => {
|
||||
setPlatformFilter(value);
|
||||
};
|
||||
|
||||
const handleStatusFilter = (value: string) => {
|
||||
setStatusFilter(value);
|
||||
};
|
||||
|
||||
const handleDateRangeChange = (dates: any) => {
|
||||
if (dates) {
|
||||
setDateRange([dates[0].format('YYYY-MM-DD'), dates[1].format('YYYY-MM-DD')]);
|
||||
} else {
|
||||
setDateRange(null);
|
||||
}
|
||||
};
|
||||
|
||||
const filteredOrders = orders.filter(order => {
|
||||
const matchesSearch = order.orderId.includes(searchText) ||
|
||||
order.merchantName.toLowerCase().includes(searchText.toLowerCase());
|
||||
const matchesMerchant = merchantFilter ? order.merchantId === merchantFilter : true;
|
||||
const matchesPlatform = platformFilter ? order.platform === platformFilter : true;
|
||||
const matchesStatus = statusFilter ? order.status === statusFilter : true;
|
||||
return matchesSearch && matchesMerchant && matchesPlatform && matchesStatus;
|
||||
});
|
||||
|
||||
const handleView = (order: Order) => {
|
||||
// 跳转到订单详情页面
|
||||
message.info(`查看订单详情: ${order.orderId}`);
|
||||
};
|
||||
|
||||
const handleStatusUpdate = (order: Order, newStatus: Order['status']) => {
|
||||
// 调用API更新订单状态
|
||||
message.info(`更新订单 ${order.orderId} 状态为: ${newStatus}`);
|
||||
};
|
||||
|
||||
const handleRefresh = () => {
|
||||
fetchOrders();
|
||||
};
|
||||
|
||||
const getStatusTag = (status: string) => {
|
||||
const statusConfig: Record<string, { color: string; text: string }> = {
|
||||
PENDING: { color: 'blue', text: '待处理' },
|
||||
PROCESSING: { color: 'orange', text: '处理中' },
|
||||
SHIPPED: { color: 'purple', text: '已发货' },
|
||||
DELIVERED: { color: 'green', text: '已送达' },
|
||||
CANCELLED: { color: 'gray', text: '已取消' },
|
||||
REFUNDED: { color: 'red', text: '已退款' },
|
||||
};
|
||||
const config = statusConfig[status] || { color: 'default', text: status };
|
||||
return <Tag color={config.color}>{config.text}</Tag>;
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '订单ID',
|
||||
dataIndex: 'id',
|
||||
key: 'id',
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: '平台订单号',
|
||||
dataIndex: 'orderId',
|
||||
key: 'orderId',
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: '商户',
|
||||
dataIndex: 'merchantName',
|
||||
key: 'merchantName',
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: '平台',
|
||||
dataIndex: 'platform',
|
||||
key: 'platform',
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: '总金额',
|
||||
dataIndex: 'totalAmount',
|
||||
key: 'totalAmount',
|
||||
render: (amount: number) => <Text>${amount.toFixed(2)}</Text>,
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
render: (status: string) => getStatusTag(status),
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'createdAt',
|
||||
key: 'createdAt',
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
render: (_: any, order: Order) => (
|
||||
<div>
|
||||
<Button
|
||||
icon={<EyeOutlined />}
|
||||
size="small"
|
||||
style={{ marginRight: 8 }}
|
||||
onClick={() => handleView(order)}
|
||||
>
|
||||
查看
|
||||
</Button>
|
||||
<Select
|
||||
defaultValue={order.status}
|
||||
style={{ width: 120 }}
|
||||
onChange={(value) => handleStatusUpdate(order, value)}
|
||||
>
|
||||
<Option value="PENDING">待处理</Option>
|
||||
<Option value="PROCESSING">处理中</Option>
|
||||
<Option value="SHIPPED">已发货</Option>
|
||||
<Option value="DELIVERED">已送达</Option>
|
||||
<Option value="CANCELLED">已取消</Option>
|
||||
<Option value="REFUNDED">已退款</Option>
|
||||
</Select>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 24 }}>
|
||||
<Title level={4}>多商户订单管理</Title>
|
||||
<Button
|
||||
icon={<ReloadOutlined />}
|
||||
onClick={handleRefresh}
|
||||
>
|
||||
刷新
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', marginBottom: 16, gap: 16, flexWrap: 'wrap' }}>
|
||||
<Input
|
||||
placeholder="搜索订单号或商户名称"
|
||||
prefix={<SearchOutlined />}
|
||||
style={{ width: 300 }}
|
||||
onChange={(e) => handleSearch(e.target.value)}
|
||||
/>
|
||||
<Select
|
||||
placeholder="筛选商户"
|
||||
style={{ width: 200 }}
|
||||
onChange={handleMerchantFilter}
|
||||
allowClear
|
||||
>
|
||||
{merchants.map(merchant => (
|
||||
<Option key={merchant.id} value={merchant.id}>
|
||||
{merchant.companyName}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
<Select
|
||||
placeholder="筛选平台"
|
||||
style={{ width: 120 }}
|
||||
onChange={handlePlatformFilter}
|
||||
allowClear
|
||||
>
|
||||
<Option value="Amazon">Amazon</Option>
|
||||
<Option value="eBay">eBay</Option>
|
||||
<Option value="Shopee">Shopee</Option>
|
||||
<Option value="TikTok">TikTok</Option>
|
||||
</Select>
|
||||
<Select
|
||||
placeholder="筛选状态"
|
||||
style={{ width: 120 }}
|
||||
onChange={handleStatusFilter}
|
||||
allowClear
|
||||
>
|
||||
<Option value="PENDING">待处理</Option>
|
||||
<Option value="PROCESSING">处理中</Option>
|
||||
<Option value="SHIPPED">已发货</Option>
|
||||
<Option value="DELIVERED">已送达</Option>
|
||||
<Option value="CANCELLED">已取消</Option>
|
||||
<Option value="REFUNDED">已退款</Option>
|
||||
</Select>
|
||||
<RangePicker
|
||||
placeholder={['开始日期', '结束日期']}
|
||||
onChange={handleDateRangeChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={filteredOrders}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
pagination={{
|
||||
showSizeChanger: true,
|
||||
pageSizeOptions: ['10', '20', '50'],
|
||||
defaultPageSize: 10,
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default MerchantOrderManage;
|
||||
315
dashboard/src/pages/Merchant/MerchantSettlementManage.tsx
Normal file
315
dashboard/src/pages/Merchant/MerchantSettlementManage.tsx
Normal file
@@ -0,0 +1,315 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Table, Input, Button, Select, DatePicker, message, Card, Typography, Tag, Modal, Form } from 'antd';
|
||||
import { SearchOutlined, EyeOutlined, DownloadOutlined, ReloadOutlined } from '@ant-design/icons';
|
||||
import { getMerchantSettlements, processSettlement } from '../../services/merchantSettlementService';
|
||||
|
||||
const { Option } = Select;
|
||||
const { Title, Text } = Typography;
|
||||
const { RangePicker } = DatePicker;
|
||||
const { Item } = Form;
|
||||
|
||||
interface Settlement {
|
||||
id: string;
|
||||
merchantId: string;
|
||||
merchantName: string;
|
||||
periodStart: string;
|
||||
periodEnd: string;
|
||||
totalAmount: number;
|
||||
status: 'PENDING' | 'PROCESSING' | 'COMPLETED' | 'FAILED';
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
const MerchantSettlementManage: React.FC = () => {
|
||||
const [settlements, setSettlements] = useState<Settlement[]>([]);
|
||||
const [merchants, setMerchants] = useState<{ id: string; companyName: string }[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [searchText, setSearchText] = useState('');
|
||||
const [merchantFilter, setMerchantFilter] = useState<string>('');
|
||||
const [statusFilter, setStatusFilter] = useState<string>('');
|
||||
const [dateRange, setDateRange] = useState<[string, string] | null>(null);
|
||||
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||
const [selectedSettlement, setSelectedSettlement] = useState<Settlement | null>(null);
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const fetchSettlements = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await getMerchantSettlements();
|
||||
setSettlements(response.data);
|
||||
} catch (error) {
|
||||
message.error('获取结算列表失败');
|
||||
console.error('获取结算列表失败:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchMerchants = async () => {
|
||||
try {
|
||||
// 实际项目中应该调用获取商户列表的API
|
||||
// 这里模拟数据
|
||||
setMerchants([
|
||||
{ id: '1', companyName: '商户A' },
|
||||
{ id: '2', companyName: '商户B' },
|
||||
{ id: '3', companyName: '商户C' },
|
||||
]);
|
||||
} catch (error) {
|
||||
console.error('获取商户列表失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchMerchants();
|
||||
fetchSettlements();
|
||||
}, []);
|
||||
|
||||
const handleSearch = (value: string) => {
|
||||
setSearchText(value);
|
||||
};
|
||||
|
||||
const handleMerchantFilter = (value: string) => {
|
||||
setMerchantFilter(value);
|
||||
};
|
||||
|
||||
const handleStatusFilter = (value: string) => {
|
||||
setStatusFilter(value);
|
||||
};
|
||||
|
||||
const handleDateRangeChange = (dates: any) => {
|
||||
if (dates) {
|
||||
setDateRange([dates[0].format('YYYY-MM-DD'), dates[1].format('YYYY-MM-DD')]);
|
||||
} else {
|
||||
setDateRange(null);
|
||||
}
|
||||
};
|
||||
|
||||
const filteredSettlements = settlements.filter(settlement => {
|
||||
const matchesSearch = settlement.id.includes(searchText) ||
|
||||
settlement.merchantName.toLowerCase().includes(searchText.toLowerCase());
|
||||
const matchesMerchant = merchantFilter ? settlement.merchantId === merchantFilter : true;
|
||||
const matchesStatus = statusFilter ? settlement.status === statusFilter : true;
|
||||
return matchesSearch && matchesMerchant && matchesStatus;
|
||||
});
|
||||
|
||||
const handleView = (settlement: Settlement) => {
|
||||
setSelectedSettlement(settlement);
|
||||
setIsModalVisible(true);
|
||||
};
|
||||
|
||||
const handleProcess = (settlement: Settlement) => {
|
||||
// 调用API处理结算
|
||||
message.info(`处理结算: ${settlement.id}`);
|
||||
};
|
||||
|
||||
const handleDownload = (settlement: Settlement) => {
|
||||
// 下载结算单
|
||||
message.info(`下载结算单: ${settlement.id}`);
|
||||
};
|
||||
|
||||
const handleRefresh = () => {
|
||||
fetchSettlements();
|
||||
};
|
||||
|
||||
const getStatusTag = (status: string) => {
|
||||
const statusConfig: Record<string, { color: string; text: string }> = {
|
||||
PENDING: { color: 'blue', text: '待处理' },
|
||||
PROCESSING: { color: 'orange', text: '处理中' },
|
||||
COMPLETED: { color: 'green', text: '已完成' },
|
||||
FAILED: { color: 'red', text: '失败' },
|
||||
};
|
||||
const config = statusConfig[status] || { color: 'default', text: status };
|
||||
return <Tag color={config.color}>{config.text}</Tag>;
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '结算单ID',
|
||||
dataIndex: 'id',
|
||||
key: 'id',
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: '商户',
|
||||
dataIndex: 'merchantName',
|
||||
key: 'merchantName',
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: '结算周期',
|
||||
key: 'period',
|
||||
render: (_, settlement: Settlement) => (
|
||||
<Text>
|
||||
{settlement.periodStart} 至 {settlement.periodEnd}
|
||||
</Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '结算金额',
|
||||
dataIndex: 'totalAmount',
|
||||
key: 'totalAmount',
|
||||
render: (amount: number) => <Text>${amount.toFixed(2)}</Text>,
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
render: (status: string) => getStatusTag(status),
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'createdAt',
|
||||
key: 'createdAt',
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
render: (_: any, settlement: Settlement) => (
|
||||
<div>
|
||||
<Button
|
||||
icon={<EyeOutlined />}
|
||||
size="small"
|
||||
style={{ marginRight: 8 }}
|
||||
onClick={() => handleView(settlement)}
|
||||
>
|
||||
查看
|
||||
</Button>
|
||||
<Button
|
||||
icon={<DownloadOutlined />}
|
||||
size="small"
|
||||
style={{ marginRight: 8 }}
|
||||
onClick={() => handleDownload(settlement)}
|
||||
>
|
||||
下载
|
||||
</Button>
|
||||
{settlement.status === 'PENDING' && (
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
onClick={() => handleProcess(settlement)}
|
||||
>
|
||||
处理
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 24 }}>
|
||||
<Title level={4}>多商户结算管理</Title>
|
||||
<Button
|
||||
icon={<ReloadOutlined />}
|
||||
onClick={handleRefresh}
|
||||
>
|
||||
刷新
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', marginBottom: 16, gap: 16, flexWrap: 'wrap' }}>
|
||||
<Input
|
||||
placeholder="搜索结算单ID或商户名称"
|
||||
prefix={<SearchOutlined />}
|
||||
style={{ width: 300 }}
|
||||
onChange={(e) => handleSearch(e.target.value)}
|
||||
/>
|
||||
<Select
|
||||
placeholder="筛选商户"
|
||||
style={{ width: 200 }}
|
||||
onChange={handleMerchantFilter}
|
||||
allowClear
|
||||
>
|
||||
{merchants.map(merchant => (
|
||||
<Option key={merchant.id} value={merchant.id}>
|
||||
{merchant.companyName}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
<Select
|
||||
placeholder="筛选状态"
|
||||
style={{ width: 120 }}
|
||||
onChange={handleStatusFilter}
|
||||
allowClear
|
||||
>
|
||||
<Option value="PENDING">待处理</Option>
|
||||
<Option value="PROCESSING">处理中</Option>
|
||||
<Option value="COMPLETED">已完成</Option>
|
||||
<Option value="FAILED">失败</Option>
|
||||
</Select>
|
||||
<RangePicker
|
||||
placeholder={['开始日期', '结束日期']}
|
||||
onChange={handleDateRangeChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={filteredSettlements}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
pagination={{
|
||||
showSizeChanger: true,
|
||||
pageSizeOptions: ['10', '20', '50'],
|
||||
defaultPageSize: 10,
|
||||
}}
|
||||
/>
|
||||
|
||||
<Modal
|
||||
title="结算详情"
|
||||
open={isModalVisible}
|
||||
onCancel={() => setIsModalVisible(false)}
|
||||
footer={[
|
||||
<Button key="close" onClick={() => setIsModalVisible(false)}>
|
||||
关闭
|
||||
</Button>,
|
||||
]}
|
||||
width={600}
|
||||
>
|
||||
{selectedSettlement && (
|
||||
<div>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<Text strong>结算单ID:</Text> {selectedSettlement.id}
|
||||
</div>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<Text strong>商户:</Text> {selectedSettlement.merchantName}
|
||||
</div>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<Text strong>结算周期:</Text> {selectedSettlement.periodStart} 至 {selectedSettlement.periodEnd}
|
||||
</div>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<Text strong>结算金额:</Text> ${selectedSettlement.totalAmount.toFixed(2)}
|
||||
</div>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<Text strong>状态:</Text> {getStatusTag(selectedSettlement.status)}
|
||||
</div>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<Text strong>创建时间:</Text> {selectedSettlement.createdAt}
|
||||
</div>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<Text strong>更新时间:</Text> {selectedSettlement.updatedAt}
|
||||
</div>
|
||||
<div style={{ marginTop: 24 }}>
|
||||
<Title level={5}>结算明细</Title>
|
||||
<Table
|
||||
columns={[
|
||||
{ title: '项目', dataIndex: 'item', key: 'item' },
|
||||
{ title: '金额', dataIndex: 'amount', key: 'amount', render: (amount: number) => `$${amount.toFixed(2)}` },
|
||||
]}
|
||||
dataSource={[
|
||||
{ key: '1', item: '商品销售', amount: selectedSettlement.totalAmount * 0.9 },
|
||||
{ key: '2', item: '平台服务费', amount: selectedSettlement.totalAmount * 0.1 },
|
||||
]}
|
||||
pagination={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default MerchantSettlementManage;
|
||||
358
dashboard/src/pages/Merchant/MerchantShopManage.tsx
Normal file
358
dashboard/src/pages/Merchant/MerchantShopManage.tsx
Normal file
@@ -0,0 +1,358 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Table, Input, Button, Select, message, Card, Typography, Form, Modal } from 'antd';
|
||||
import { SearchOutlined, PlusOutlined, EditOutlined, DeleteOutlined, EyeOutlined } from '@ant-design/icons';
|
||||
import { getMerchantShops, createMerchantShop, updateMerchantShop, deleteMerchantShop } from '../../services/merchantShopService';
|
||||
|
||||
const { Option } = Select;
|
||||
const { Title, Text } = Typography;
|
||||
const { Item } = Form;
|
||||
|
||||
interface Shop {
|
||||
id: string;
|
||||
merchantId: string;
|
||||
shopName: string;
|
||||
platform: string;
|
||||
shopUrl: string;
|
||||
status: 'ACTIVE' | 'INACTIVE' | 'SUSPENDED';
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
const MerchantShopManage: React.FC = () => {
|
||||
const [shops, setShops] = useState<Shop[]>([]);
|
||||
const [merchants, setMerchants] = useState<{ id: string; companyName: string }[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [searchText, setSearchText] = useState('');
|
||||
const [merchantFilter, setMerchantFilter] = useState<string>('');
|
||||
const [platformFilter, setPlatformFilter] = useState<string>('');
|
||||
const [statusFilter, setStatusFilter] = useState<string>('');
|
||||
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||
const [form] = Form.useForm();
|
||||
const [editingShop, setEditingShop] = useState<Shop | null>(null);
|
||||
|
||||
const fetchShops = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await getMerchantShops();
|
||||
setShops(response.data);
|
||||
} catch (error) {
|
||||
message.error('获取店铺列表失败');
|
||||
console.error('获取店铺列表失败:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchMerchants = async () => {
|
||||
try {
|
||||
// 实际项目中应该调用获取商户列表的API
|
||||
// 这里模拟数据
|
||||
setMerchants([
|
||||
{ id: '1', companyName: '商户A' },
|
||||
{ id: '2', companyName: '商户B' },
|
||||
{ id: '3', companyName: '商户C' },
|
||||
]);
|
||||
} catch (error) {
|
||||
console.error('获取商户列表失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchMerchants();
|
||||
fetchShops();
|
||||
}, []);
|
||||
|
||||
const handleSearch = (value: string) => {
|
||||
setSearchText(value);
|
||||
};
|
||||
|
||||
const handleMerchantFilter = (value: string) => {
|
||||
setMerchantFilter(value);
|
||||
};
|
||||
|
||||
const handlePlatformFilter = (value: string) => {
|
||||
setPlatformFilter(value);
|
||||
};
|
||||
|
||||
const handleStatusFilter = (value: string) => {
|
||||
setStatusFilter(value);
|
||||
};
|
||||
|
||||
const filteredShops = shops.filter(shop => {
|
||||
const matchesSearch = shop.shopName.toLowerCase().includes(searchText.toLowerCase()) ||
|
||||
shop.shopUrl.toLowerCase().includes(searchText.toLowerCase());
|
||||
const matchesMerchant = merchantFilter ? shop.merchantId === merchantFilter : true;
|
||||
const matchesPlatform = platformFilter ? shop.platform === platformFilter : true;
|
||||
const matchesStatus = statusFilter ? shop.status === statusFilter : true;
|
||||
return matchesSearch && matchesMerchant && matchesPlatform && matchesStatus;
|
||||
});
|
||||
|
||||
const handleCreate = () => {
|
||||
setEditingShop(null);
|
||||
form.resetFields();
|
||||
setIsModalVisible(true);
|
||||
};
|
||||
|
||||
const handleEdit = (shop: Shop) => {
|
||||
setEditingShop(shop);
|
||||
form.setFieldsValue(shop);
|
||||
setIsModalVisible(true);
|
||||
};
|
||||
|
||||
const handleDelete = (shopId: string) => {
|
||||
// 确认删除后调用API
|
||||
message.info(`删除店铺: ${shopId}`);
|
||||
};
|
||||
|
||||
const handleView = (shop: Shop) => {
|
||||
// 跳转到店铺详情页面
|
||||
message.info(`查看店铺详情: ${shop.shopName}`);
|
||||
};
|
||||
|
||||
const handleOk = async () => {
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
if (editingShop) {
|
||||
// 编辑店铺
|
||||
await updateMerchantShop(editingShop.id, values);
|
||||
message.success('店铺更新成功');
|
||||
} else {
|
||||
// 创建店铺
|
||||
await createMerchantShop(values);
|
||||
message.success('店铺创建成功');
|
||||
}
|
||||
setIsModalVisible(false);
|
||||
fetchShops();
|
||||
} catch (error) {
|
||||
message.error('操作失败');
|
||||
console.error('操作失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
setIsModalVisible(false);
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '店铺ID',
|
||||
dataIndex: 'id',
|
||||
key: 'id',
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: '商户',
|
||||
dataIndex: 'merchantId',
|
||||
key: 'merchantId',
|
||||
render: (merchantId: string) => {
|
||||
const merchant = merchants.find(m => m.id === merchantId);
|
||||
return <Text>{merchant?.companyName || merchantId}</Text>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '店铺名称',
|
||||
dataIndex: 'shopName',
|
||||
key: 'shopName',
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: '平台',
|
||||
dataIndex: 'platform',
|
||||
key: 'platform',
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: '店铺链接',
|
||||
dataIndex: 'shopUrl',
|
||||
key: 'shopUrl',
|
||||
ellipsis: true,
|
||||
render: (shopUrl: string) => (
|
||||
<a href={shopUrl} target="_blank" rel="noopener noreferrer">
|
||||
{shopUrl}
|
||||
</a>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
render: (status: string) => {
|
||||
const statusMap: Record<string, string> = {
|
||||
ACTIVE: '活跃',
|
||||
INACTIVE: '未激活',
|
||||
SUSPENDED: '暂停',
|
||||
};
|
||||
return <Text>{statusMap[status] || status}</Text>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'createdAt',
|
||||
key: 'createdAt',
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
render: (_: any, shop: Shop) => (
|
||||
<div>
|
||||
<Button
|
||||
icon={<EyeOutlined />}
|
||||
size="small"
|
||||
style={{ marginRight: 8 }}
|
||||
onClick={() => handleView(shop)}
|
||||
>
|
||||
查看
|
||||
</Button>
|
||||
<Button
|
||||
icon={<EditOutlined />}
|
||||
size="small"
|
||||
style={{ marginRight: 8 }}
|
||||
onClick={() => handleEdit(shop)}
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
<Button
|
||||
icon={<DeleteOutlined />}
|
||||
size="small"
|
||||
danger
|
||||
onClick={() => handleDelete(shop.id)}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 24 }}>
|
||||
<Title level={4}>商户店铺管理</Title>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={handleCreate}
|
||||
>
|
||||
新增店铺
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', marginBottom: 16, gap: 16, flexWrap: 'wrap' }}>
|
||||
<Input
|
||||
placeholder="搜索店铺名称或链接"
|
||||
prefix={<SearchOutlined />}
|
||||
style={{ width: 300 }}
|
||||
onChange={(e) => handleSearch(e.target.value)}
|
||||
/>
|
||||
<Select
|
||||
placeholder="筛选商户"
|
||||
style={{ width: 200 }}
|
||||
onChange={handleMerchantFilter}
|
||||
allowClear
|
||||
>
|
||||
{merchants.map(merchant => (
|
||||
<Option key={merchant.id} value={merchant.id}>
|
||||
{merchant.companyName}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
<Select
|
||||
placeholder="筛选平台"
|
||||
style={{ width: 120 }}
|
||||
onChange={handlePlatformFilter}
|
||||
allowClear
|
||||
>
|
||||
<Option value="Amazon">Amazon</Option>
|
||||
<Option value="eBay">eBay</Option>
|
||||
<Option value="Shopee">Shopee</Option>
|
||||
<Option value="TikTok">TikTok</Option>
|
||||
</Select>
|
||||
<Select
|
||||
placeholder="筛选状态"
|
||||
style={{ width: 120 }}
|
||||
onChange={handleStatusFilter}
|
||||
allowClear
|
||||
>
|
||||
<Option value="ACTIVE">活跃</Option>
|
||||
<Option value="INACTIVE">未激活</Option>
|
||||
<Option value="SUSPENDED">暂停</Option>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={filteredShops}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
pagination={{
|
||||
showSizeChanger: true,
|
||||
pageSizeOptions: ['10', '20', '50'],
|
||||
defaultPageSize: 10,
|
||||
}}
|
||||
/>
|
||||
|
||||
<Modal
|
||||
title={editingShop ? '编辑店铺' : '新增店铺'}
|
||||
open={isModalVisible}
|
||||
onOk={handleOk}
|
||||
onCancel={handleCancel}
|
||||
>
|
||||
<Form form={form} layout="vertical">
|
||||
<Item
|
||||
name="merchantId"
|
||||
label="商户"
|
||||
rules={[{ required: true, message: '请选择商户' }]}
|
||||
>
|
||||
<Select placeholder="选择商户">
|
||||
{merchants.map(merchant => (
|
||||
<Option key={merchant.id} value={merchant.id}>
|
||||
{merchant.companyName}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Item>
|
||||
<Item
|
||||
name="shopName"
|
||||
label="店铺名称"
|
||||
rules={[{ required: true, message: '请输入店铺名称' }]}
|
||||
>
|
||||
<Input placeholder="请输入店铺名称" />
|
||||
</Item>
|
||||
<Item
|
||||
name="platform"
|
||||
label="平台"
|
||||
rules={[{ required: true, message: '请选择平台' }]}
|
||||
>
|
||||
<Select placeholder="选择平台">
|
||||
<Option value="Amazon">Amazon</Option>
|
||||
<Option value="eBay">eBay</Option>
|
||||
<Option value="Shopee">Shopee</Option>
|
||||
<Option value="TikTok">TikTok</Option>
|
||||
</Select>
|
||||
</Item>
|
||||
<Item
|
||||
name="shopUrl"
|
||||
label="店铺链接"
|
||||
rules={[{ required: true, message: '请输入店铺链接' }]}
|
||||
>
|
||||
<Input placeholder="请输入店铺链接" />
|
||||
</Item>
|
||||
<Item
|
||||
name="status"
|
||||
label="状态"
|
||||
rules={[{ required: true, message: '请选择状态' }]}
|
||||
>
|
||||
<Select placeholder="选择状态">
|
||||
<Option value="ACTIVE">活跃</Option>
|
||||
<Option value="INACTIVE">未激活</Option>
|
||||
<Option value="SUSPENDED">暂停</Option>
|
||||
</Select>
|
||||
</Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default MerchantShopManage;
|
||||
18
dashboard/src/pages/Merchant/index.ts
Normal file
18
dashboard/src/pages/Merchant/index.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import MerchantManage from './MerchantManage';
|
||||
import MerchantShopManage from './MerchantShopManage';
|
||||
import MerchantOrderManage from './MerchantOrderManage';
|
||||
import MerchantSettlementManage from './MerchantSettlementManage';
|
||||
|
||||
export {
|
||||
MerchantManage,
|
||||
MerchantShopManage,
|
||||
MerchantOrderManage,
|
||||
MerchantSettlementManage,
|
||||
};
|
||||
|
||||
export default {
|
||||
MerchantManage,
|
||||
MerchantShopManage,
|
||||
MerchantOrderManage,
|
||||
MerchantSettlementManage,
|
||||
};
|
||||
135
dashboard/src/services/merchantOrderService.ts
Normal file
135
dashboard/src/services/merchantOrderService.ts
Normal file
@@ -0,0 +1,135 @@
|
||||
// 商户订单管理服务
|
||||
|
||||
interface Order {
|
||||
id: string;
|
||||
merchantId: string;
|
||||
merchantName: string;
|
||||
orderId: string;
|
||||
platform: string;
|
||||
totalAmount: number;
|
||||
status: 'PENDING' | 'PROCESSING' | 'SHIPPED' | 'DELIVERED' | 'CANCELLED' | 'REFUNDED';
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
interface OrderResponse {
|
||||
data: Order[];
|
||||
total: number;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
}
|
||||
|
||||
// 模拟数据
|
||||
const mockOrders: Order[] = [
|
||||
{
|
||||
id: '1',
|
||||
merchantId: '1',
|
||||
merchantName: '商户A',
|
||||
orderId: 'AMZ123456',
|
||||
platform: 'Amazon',
|
||||
totalAmount: 100.50,
|
||||
status: 'DELIVERED',
|
||||
createdAt: '2026-03-01T10:00:00Z',
|
||||
updatedAt: '2026-03-03T10:00:00Z',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
merchantId: '1',
|
||||
merchantName: '商户A',
|
||||
orderId: 'EBAY789012',
|
||||
platform: 'eBay',
|
||||
totalAmount: 50.25,
|
||||
status: 'SHIPPED',
|
||||
createdAt: '2026-03-02T10:00:00Z',
|
||||
updatedAt: '2026-03-03T10:00:00Z',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
merchantId: '2',
|
||||
merchantName: '商户B',
|
||||
orderId: 'SHOPEE345678',
|
||||
platform: 'Shopee',
|
||||
totalAmount: 75.75,
|
||||
status: 'PROCESSING',
|
||||
createdAt: '2026-03-03T10:00:00Z',
|
||||
updatedAt: '2026-03-03T10:00:00Z',
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
merchantId: '3',
|
||||
merchantName: '商户C',
|
||||
orderId: 'TIKTOK901234',
|
||||
platform: 'TikTok',
|
||||
totalAmount: 120.00,
|
||||
status: 'PENDING',
|
||||
createdAt: '2026-03-04T10:00:00Z',
|
||||
updatedAt: '2026-03-04T10:00:00Z',
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
merchantId: '1',
|
||||
merchantName: '商户A',
|
||||
orderId: 'AMZ567890',
|
||||
platform: 'Amazon',
|
||||
totalAmount: 200.00,
|
||||
status: 'CANCELLED',
|
||||
createdAt: '2026-03-05T10:00:00Z',
|
||||
updatedAt: '2026-03-05T10:00:00Z',
|
||||
},
|
||||
];
|
||||
|
||||
// 获取商户订单列表
|
||||
export const getMerchantOrders = async (): Promise<OrderResponse> => {
|
||||
// 模拟API请求延迟
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
return {
|
||||
data: mockOrders,
|
||||
total: mockOrders.length,
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
};
|
||||
};
|
||||
|
||||
// 根据商户ID获取订单列表
|
||||
export const getOrdersByMerchantId = async (merchantId: string): Promise<Order[]> => {
|
||||
// 模拟API请求延迟
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
return mockOrders.filter(o => o.merchantId === merchantId);
|
||||
};
|
||||
|
||||
// 更新订单状态
|
||||
export const updateOrderStatus = async (orderId: string, status: Order['status']): Promise<Order> => {
|
||||
// 模拟API请求延迟
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
const index = mockOrders.findIndex(o => o.id === orderId);
|
||||
if (index === -1) {
|
||||
throw new Error('订单不存在');
|
||||
}
|
||||
mockOrders[index] = {
|
||||
...mockOrders[index],
|
||||
status,
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
return mockOrders[index];
|
||||
};
|
||||
|
||||
// 获取订单详情
|
||||
export const getOrderById = async (orderId: string): Promise<Order> => {
|
||||
// 模拟API请求延迟
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
const order = mockOrders.find(o => o.id === orderId);
|
||||
if (!order) {
|
||||
throw new Error('订单不存在');
|
||||
}
|
||||
return order;
|
||||
};
|
||||
|
||||
// 搜索订单
|
||||
export const searchOrders = async (keyword: string): Promise<Order[]> => {
|
||||
// 模拟API请求延迟
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
return mockOrders.filter(o =>
|
||||
o.orderId.includes(keyword) ||
|
||||
o.merchantName.toLowerCase().includes(keyword.toLowerCase())
|
||||
);
|
||||
};
|
||||
137
dashboard/src/services/merchantService.ts
Normal file
137
dashboard/src/services/merchantService.ts
Normal file
@@ -0,0 +1,137 @@
|
||||
// 商户管理服务
|
||||
|
||||
interface Merchant {
|
||||
id: string;
|
||||
tenantId: string;
|
||||
companyName: string;
|
||||
businessLicense: string;
|
||||
contactEmail: string;
|
||||
contactPhone: string;
|
||||
status: 'PENDING' | 'ACTIVE' | 'SUSPENDED' | 'TERMINATED';
|
||||
tier: 'BASIC' | 'PRO' | 'ENTERPRISE';
|
||||
traceId: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
interface MerchantResponse {
|
||||
data: Merchant[];
|
||||
total: number;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
}
|
||||
|
||||
// 模拟数据
|
||||
const mockMerchants: Merchant[] = [
|
||||
{
|
||||
id: '1',
|
||||
tenantId: 'tenant1',
|
||||
companyName: '商户A',
|
||||
businessLicense: '1234567890',
|
||||
contactEmail: 'merchantA@example.com',
|
||||
contactPhone: '13800138001',
|
||||
status: 'ACTIVE',
|
||||
tier: 'PRO',
|
||||
traceId: 'trace1',
|
||||
createdAt: '2026-03-01T10:00:00Z',
|
||||
updatedAt: '2026-03-01T10:00:00Z',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
tenantId: 'tenant2',
|
||||
companyName: '商户B',
|
||||
businessLicense: '0987654321',
|
||||
contactEmail: 'merchantB@example.com',
|
||||
contactPhone: '13900139001',
|
||||
status: 'PENDING',
|
||||
tier: 'BASIC',
|
||||
traceId: 'trace2',
|
||||
createdAt: '2026-03-02T10:00:00Z',
|
||||
updatedAt: '2026-03-02T10:00:00Z',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
tenantId: 'tenant3',
|
||||
companyName: '商户C',
|
||||
businessLicense: '1122334455',
|
||||
contactEmail: 'merchantC@example.com',
|
||||
contactPhone: '13700137001',
|
||||
status: 'SUSPENDED',
|
||||
tier: 'ENTERPRISE',
|
||||
traceId: 'trace3',
|
||||
createdAt: '2026-03-03T10:00:00Z',
|
||||
updatedAt: '2026-03-03T10:00:00Z',
|
||||
},
|
||||
];
|
||||
|
||||
// 获取商户列表
|
||||
export const getMerchants = async (): Promise<MerchantResponse> => {
|
||||
// 模拟API请求延迟
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
return {
|
||||
data: mockMerchants,
|
||||
total: mockMerchants.length,
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
};
|
||||
};
|
||||
|
||||
// 创建商户
|
||||
export const createMerchant = async (merchantData: Partial<Merchant>): Promise<Merchant> => {
|
||||
// 模拟API请求延迟
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
const newMerchant: Merchant = {
|
||||
id: `new-${Date.now()}`,
|
||||
tenantId: merchantData.tenantId || 'default-tenant',
|
||||
companyName: merchantData.companyName || '',
|
||||
businessLicense: merchantData.businessLicense || '',
|
||||
contactEmail: merchantData.contactEmail || '',
|
||||
contactPhone: merchantData.contactPhone || '',
|
||||
status: merchantData.status || 'PENDING',
|
||||
tier: merchantData.tier || 'BASIC',
|
||||
traceId: `trace-${Date.now()}`,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
mockMerchants.push(newMerchant);
|
||||
return newMerchant;
|
||||
};
|
||||
|
||||
// 更新商户
|
||||
export const updateMerchant = async (merchantId: string, merchantData: Partial<Merchant>): Promise<Merchant> => {
|
||||
// 模拟API请求延迟
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
const index = mockMerchants.findIndex(m => m.id === merchantId);
|
||||
if (index === -1) {
|
||||
throw new Error('商户不存在');
|
||||
}
|
||||
mockMerchants[index] = {
|
||||
...mockMerchants[index],
|
||||
...merchantData,
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
return mockMerchants[index];
|
||||
};
|
||||
|
||||
// 删除商户
|
||||
export const deleteMerchant = async (merchantId: string): Promise<boolean> => {
|
||||
// 模拟API请求延迟
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
const index = mockMerchants.findIndex(m => m.id === merchantId);
|
||||
if (index === -1) {
|
||||
throw new Error('商户不存在');
|
||||
}
|
||||
mockMerchants.splice(index, 1);
|
||||
return true;
|
||||
};
|
||||
|
||||
// 获取商户详情
|
||||
export const getMerchantById = async (merchantId: string): Promise<Merchant> => {
|
||||
// 模拟API请求延迟
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
const merchant = mockMerchants.find(m => m.id === merchantId);
|
||||
if (!merchant) {
|
||||
throw new Error('商户不存在');
|
||||
}
|
||||
return merchant;
|
||||
};
|
||||
133
dashboard/src/services/merchantSettlementService.ts
Normal file
133
dashboard/src/services/merchantSettlementService.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
// 商户结算管理服务
|
||||
|
||||
interface Settlement {
|
||||
id: string;
|
||||
merchantId: string;
|
||||
merchantName: string;
|
||||
periodStart: string;
|
||||
periodEnd: string;
|
||||
totalAmount: number;
|
||||
status: 'PENDING' | 'PROCESSING' | 'COMPLETED' | 'FAILED';
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
interface SettlementResponse {
|
||||
data: Settlement[];
|
||||
total: number;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
}
|
||||
|
||||
// 模拟数据
|
||||
const mockSettlements: Settlement[] = [
|
||||
{
|
||||
id: '1',
|
||||
merchantId: '1',
|
||||
merchantName: '商户A',
|
||||
periodStart: '2026-02-01',
|
||||
periodEnd: '2026-02-29',
|
||||
totalAmount: 15000.00,
|
||||
status: 'COMPLETED',
|
||||
createdAt: '2026-03-01T10:00:00Z',
|
||||
updatedAt: '2026-03-02T10:00:00Z',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
merchantId: '2',
|
||||
merchantName: '商户B',
|
||||
periodStart: '2026-02-01',
|
||||
periodEnd: '2026-02-29',
|
||||
totalAmount: 8000.00,
|
||||
status: 'COMPLETED',
|
||||
createdAt: '2026-03-01T10:00:00Z',
|
||||
updatedAt: '2026-03-02T10:00:00Z',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
merchantId: '3',
|
||||
merchantName: '商户C',
|
||||
periodStart: '2026-02-01',
|
||||
periodEnd: '2026-02-29',
|
||||
totalAmount: 12000.00,
|
||||
status: 'PROCESSING',
|
||||
createdAt: '2026-03-01T10:00:00Z',
|
||||
updatedAt: '2026-03-01T10:00:00Z',
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
merchantId: '1',
|
||||
merchantName: '商户A',
|
||||
periodStart: '2026-03-01',
|
||||
periodEnd: '2026-03-31',
|
||||
totalAmount: 0.00,
|
||||
status: 'PENDING',
|
||||
createdAt: '2026-04-01T10:00:00Z',
|
||||
updatedAt: '2026-04-01T10:00:00Z',
|
||||
},
|
||||
];
|
||||
|
||||
// 获取商户结算列表
|
||||
export const getMerchantSettlements = async (): Promise<SettlementResponse> => {
|
||||
// 模拟API请求延迟
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
return {
|
||||
data: mockSettlements,
|
||||
total: mockSettlements.length,
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
};
|
||||
};
|
||||
|
||||
// 根据商户ID获取结算列表
|
||||
export const getSettlementsByMerchantId = async (merchantId: string): Promise<Settlement[]> => {
|
||||
// 模拟API请求延迟
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
return mockSettlements.filter(s => s.merchantId === merchantId);
|
||||
};
|
||||
|
||||
// 处理结算
|
||||
export const processSettlement = async (settlementId: string): Promise<Settlement> => {
|
||||
// 模拟API请求延迟
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
const index = mockSettlements.findIndex(s => s.id === settlementId);
|
||||
if (index === -1) {
|
||||
throw new Error('结算单不存在');
|
||||
}
|
||||
mockSettlements[index] = {
|
||||
...mockSettlements[index],
|
||||
status: 'COMPLETED',
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
return mockSettlements[index];
|
||||
};
|
||||
|
||||
// 获取结算详情
|
||||
export const getSettlementById = async (settlementId: string): Promise<Settlement> => {
|
||||
// 模拟API请求延迟
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
const settlement = mockSettlements.find(s => s.id === settlementId);
|
||||
if (!settlement) {
|
||||
throw new Error('结算单不存在');
|
||||
}
|
||||
return settlement;
|
||||
};
|
||||
|
||||
// 创建结算单
|
||||
export const createSettlement = async (settlementData: Partial<Settlement>): Promise<Settlement> => {
|
||||
// 模拟API请求延迟
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
const newSettlement: Settlement = {
|
||||
id: `new-${Date.now()}`,
|
||||
merchantId: settlementData.merchantId || '',
|
||||
merchantName: settlementData.merchantName || '',
|
||||
periodStart: settlementData.periodStart || '',
|
||||
periodEnd: settlementData.periodEnd || '',
|
||||
totalAmount: settlementData.totalAmount || 0,
|
||||
status: 'PENDING',
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
mockSettlements.push(newSettlement);
|
||||
return newSettlement;
|
||||
};
|
||||
139
dashboard/src/services/merchantShopService.ts
Normal file
139
dashboard/src/services/merchantShopService.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
// 商户店铺管理服务
|
||||
|
||||
interface Shop {
|
||||
id: string;
|
||||
merchantId: string;
|
||||
shopName: string;
|
||||
platform: string;
|
||||
shopUrl: string;
|
||||
status: 'ACTIVE' | 'INACTIVE' | 'SUSPENDED';
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
interface ShopResponse {
|
||||
data: Shop[];
|
||||
total: number;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
}
|
||||
|
||||
// 模拟数据
|
||||
const mockShops: Shop[] = [
|
||||
{
|
||||
id: '1',
|
||||
merchantId: '1',
|
||||
shopName: '商户A的Amazon店铺',
|
||||
platform: 'Amazon',
|
||||
shopUrl: 'https://www.amazon.com/shop/merchantA',
|
||||
status: 'ACTIVE',
|
||||
createdAt: '2026-03-01T10:00:00Z',
|
||||
updatedAt: '2026-03-01T10:00:00Z',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
merchantId: '1',
|
||||
shopName: '商户A的eBay店铺',
|
||||
platform: 'eBay',
|
||||
shopUrl: 'https://www.ebay.com/str/merchantA',
|
||||
status: 'ACTIVE',
|
||||
createdAt: '2026-03-02T10:00:00Z',
|
||||
updatedAt: '2026-03-02T10:00:00Z',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
merchantId: '2',
|
||||
shopName: '商户B的Shopee店铺',
|
||||
platform: 'Shopee',
|
||||
shopUrl: 'https://shopee.com.my/merchantB',
|
||||
status: 'INACTIVE',
|
||||
createdAt: '2026-03-03T10:00:00Z',
|
||||
updatedAt: '2026-03-03T10:00:00Z',
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
merchantId: '3',
|
||||
shopName: '商户C的TikTok店铺',
|
||||
platform: 'TikTok',
|
||||
shopUrl: 'https://www.tiktok.com/@merchantC',
|
||||
status: 'SUSPENDED',
|
||||
createdAt: '2026-03-04T10:00:00Z',
|
||||
updatedAt: '2026-03-04T10:00:00Z',
|
||||
},
|
||||
];
|
||||
|
||||
// 获取商户店铺列表
|
||||
export const getMerchantShops = async (): Promise<ShopResponse> => {
|
||||
// 模拟API请求延迟
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
return {
|
||||
data: mockShops,
|
||||
total: mockShops.length,
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
};
|
||||
};
|
||||
|
||||
// 创建商户店铺
|
||||
export const createMerchantShop = async (shopData: Partial<Shop>): Promise<Shop> => {
|
||||
// 模拟API请求延迟
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
const newShop: Shop = {
|
||||
id: `new-${Date.now()}`,
|
||||
merchantId: shopData.merchantId || '',
|
||||
shopName: shopData.shopName || '',
|
||||
platform: shopData.platform || '',
|
||||
shopUrl: shopData.shopUrl || '',
|
||||
status: shopData.status || 'INACTIVE',
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
mockShops.push(newShop);
|
||||
return newShop;
|
||||
};
|
||||
|
||||
// 更新商户店铺
|
||||
export const updateMerchantShop = async (shopId: string, shopData: Partial<Shop>): Promise<Shop> => {
|
||||
// 模拟API请求延迟
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
const index = mockShops.findIndex(s => s.id === shopId);
|
||||
if (index === -1) {
|
||||
throw new Error('店铺不存在');
|
||||
}
|
||||
mockShops[index] = {
|
||||
...mockShops[index],
|
||||
...shopData,
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
return mockShops[index];
|
||||
};
|
||||
|
||||
// 删除商户店铺
|
||||
export const deleteMerchantShop = async (shopId: string): Promise<boolean> => {
|
||||
// 模拟API请求延迟
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
const index = mockShops.findIndex(s => s.id === shopId);
|
||||
if (index === -1) {
|
||||
throw new Error('店铺不存在');
|
||||
}
|
||||
mockShops.splice(index, 1);
|
||||
return true;
|
||||
};
|
||||
|
||||
// 获取店铺详情
|
||||
export const getShopById = async (shopId: string): Promise<Shop> => {
|
||||
// 模拟API请求延迟
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
const shop = mockShops.find(s => s.id === shopId);
|
||||
if (!shop) {
|
||||
throw new Error('店铺不存在');
|
||||
}
|
||||
return shop;
|
||||
};
|
||||
|
||||
// 根据商户ID获取店铺列表
|
||||
export const getShopsByMerchantId = async (merchantId: string): Promise<Shop[]> => {
|
||||
// 模拟API请求延迟
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
return mockShops.filter(s => s.merchantId === merchantId);
|
||||
};
|
||||
Reference in New Issue
Block a user