refactor: 重构项目结构并优化类型定义
- 移除extension模块,将功能迁移至node-agent - 修复类型导出问题,使用export type明确类型导出 - 统一数据库连接方式,从直接导入改为使用config/database - 更新文档中的项目结构描述 - 添加多个服务的实用方法,如getForecast、getBalances等 - 修复类型错误和TS1205警告 - 优化RedisService调用方式 - 添加新的实体类型定义 - 更新审计日志格式,统一字段命名
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
|
||||
// FE-EXP003: 前端组件库优化
|
||||
import React from 'react';
|
||||
import { Button, Card, Input, Table, Form, Select, DatePicker, Modal, message, Divider, Badge, Tag, Tooltip, Popconfirm, Switch, Radio, Checkbox, Slider, Rate, Upload, Progress, Alert, Space, Typography } from 'antd';
|
||||
import dayjs from 'dayjs';
|
||||
import { UploadOutlined, PlusOutlined, DeleteOutlined, EditOutlined, EyeOutlined, CheckOutlined, CloseOutlined, InfoCircleOutlined, WarningOutlined, ErrorOutlined, SuccessOutlined } from '@ant-design/icons';
|
||||
import { UploadOutlined, PlusOutlined, DeleteOutlined, EditOutlined, EyeOutlined, CheckOutlined, CloseOutlined, InfoCircleOutlined, WarningOutlined, CheckCircleOutlined, CloseCircleOutlined } from '@ant-design/icons';
|
||||
|
||||
const { Text, Paragraph, Title } = Typography;
|
||||
|
||||
@@ -434,8 +435,8 @@ export const Icons = {
|
||||
CloseOutlined,
|
||||
InfoCircleOutlined,
|
||||
WarningOutlined,
|
||||
ErrorOutlined,
|
||||
SuccessOutlined,
|
||||
CheckCircleOutlined,
|
||||
CloseCircleOutlined,
|
||||
};
|
||||
|
||||
export default {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { InputHTMLAttributes } from 'react';
|
||||
|
||||
interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
|
||||
interface InputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'size' | 'prefix'> {
|
||||
label?: string;
|
||||
error?: string;
|
||||
disabled?: boolean;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// FE-EXP002: 前端性能优化
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { Spin, Alert, Button, Card, Row, Col, Progress, Statistic, Tooltip } from 'antd';
|
||||
import { LoadingOutlined, CheckCircleOutlined, AlertOutlined, ClockCircleOutlined, ZapOutlined, DatabaseOutlined } from '@ant-design/icons';
|
||||
import { LoadingOutlined, CheckCircleOutlined, AlertOutlined, ClockCircleOutlined, ApiOutlined, DatabaseOutlined } from '@ant-design/icons';
|
||||
|
||||
const { Meta } = Card;
|
||||
|
||||
@@ -95,7 +95,7 @@ export const PerformanceMonitor: React.FC<{
|
||||
<Col span={8}>
|
||||
<Card>
|
||||
<Meta
|
||||
avatar={<ZapOutlined style={{ fontSize: '24px', color: '#52c41a' }} />}
|
||||
avatar={<ApiOutlined style={{ fontSize: '24px', color: '#52c41a' }} />}
|
||||
title="Render Time"
|
||||
description={`${metrics.renderTime.toFixed(0)}ms`}
|
||||
/>
|
||||
@@ -285,7 +285,7 @@ export const PerformanceSuggestions: React.FC = () => {
|
||||
{
|
||||
title: 'Use Lazy Loading',
|
||||
description: 'Load components only when they are needed to reduce initial load time.',
|
||||
icon: <ZapOutlined />,
|
||||
icon: <ApiOutlined />,
|
||||
severity: 'high',
|
||||
},
|
||||
{
|
||||
@@ -297,7 +297,7 @@ export const PerformanceSuggestions: React.FC = () => {
|
||||
{
|
||||
title: 'Minimize JavaScript',
|
||||
description: 'Bundle and minify JavaScript files to reduce file size.',
|
||||
icon: <ZapOutlined />,
|
||||
icon: <ApiOutlined />,
|
||||
severity: 'high',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -28,18 +28,12 @@ import {
|
||||
ThunderboltOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { Link, useLocation, useNavigate, Outlet } from 'react-router-dom';
|
||||
import type { MenuProps } from 'antd';
|
||||
|
||||
const { Header, Sider, Content } = Layout;
|
||||
const { Title, Text } = Typography;
|
||||
|
||||
// 菜单项类型定义
|
||||
interface MenuItem {
|
||||
key: string;
|
||||
icon?: React.ReactNode;
|
||||
label: React.ReactNode;
|
||||
children?: MenuItem[];
|
||||
type?: 'divider';
|
||||
}
|
||||
type MenuItem = MenuProps['items'][number];
|
||||
|
||||
// 菜单项配置
|
||||
const menuItems: MenuItem[] = [
|
||||
@@ -194,6 +188,7 @@ const userMenuItems: MenuItem[] = [
|
||||
label: '账号设置',
|
||||
},
|
||||
{
|
||||
key: 'divider-1',
|
||||
type: 'divider',
|
||||
},
|
||||
{
|
||||
@@ -211,49 +206,22 @@ const MainLayout: React.FC = () => {
|
||||
// 根据当前路径设置选中的菜单项
|
||||
useEffect(() => {
|
||||
const pathname = location.pathname;
|
||||
// 找到匹配的菜单项
|
||||
let matchedKey: string | undefined;
|
||||
|
||||
// 递归查找匹配的菜单项
|
||||
const findMatchedItem = (items: MenuItem[]): boolean => {
|
||||
const findKey = (items: MenuItem[]): string | undefined => {
|
||||
for (const item of items) {
|
||||
if (item.type === 'divider') continue;
|
||||
if (item.children) {
|
||||
// 检查子菜单项
|
||||
for (const child of item.children) {
|
||||
if (child.type === 'divider') continue;
|
||||
if (child.key === '/') {
|
||||
if (pathname === '/') {
|
||||
matchedKey = child.key;
|
||||
return true;
|
||||
}
|
||||
} else if (pathname.startsWith(child.key)) {
|
||||
matchedKey = child.key;
|
||||
return true;
|
||||
}
|
||||
if (item && 'key' in item && typeof item.key === 'string') {
|
||||
if (item.key.startsWith('/') && pathname.startsWith(item.key)) {
|
||||
return item.key;
|
||||
}
|
||||
// 递归检查子菜单
|
||||
if (findMatchedItem(item.children)) {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
// 检查当前菜单项
|
||||
if (item.key === '/') {
|
||||
if (pathname === '/') {
|
||||
matchedKey = item.key;
|
||||
return true;
|
||||
}
|
||||
} else if (pathname.startsWith(item.key)) {
|
||||
matchedKey = item.key;
|
||||
return true;
|
||||
if ('children' in item && Array.isArray(item.children)) {
|
||||
const found = findKey(item.children as MenuItem[]);
|
||||
if (found) return found;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
return undefined;
|
||||
};
|
||||
|
||||
findMatchedItem(menuItems);
|
||||
|
||||
const matchedKey = findKey(menuItems);
|
||||
if (matchedKey) {
|
||||
setSelectedKeys([matchedKey]);
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
* @created 2026-03-19
|
||||
*/
|
||||
|
||||
import { Certificate } from '@/types/certificate';
|
||||
import { Certificate, CertificateType, CertificateStatus } from '@/types/certificate';
|
||||
|
||||
/**
|
||||
* 证书Mock数据列表
|
||||
@@ -16,8 +16,8 @@ export const mockCertificates: Certificate[] = [
|
||||
{
|
||||
id: '1',
|
||||
name: 'CE认证证书',
|
||||
type: 'PRODUCT_CERT',
|
||||
status: 'APPROVED',
|
||||
type: CertificateType.PRODUCT_CERT,
|
||||
status: CertificateStatus.APPROVED,
|
||||
fileUrl: '/files/ce-cert.pdf',
|
||||
fileName: 'CE-Certificate-2026.pdf',
|
||||
uploadDate: '2026-03-15',
|
||||
@@ -31,8 +31,8 @@ export const mockCertificates: Certificate[] = [
|
||||
{
|
||||
id: '2',
|
||||
name: '营业执照',
|
||||
type: 'BUSINESS_LICENSE',
|
||||
status: 'APPROVED',
|
||||
type: CertificateType.BUSINESS_LICENSE,
|
||||
status: CertificateStatus.APPROVED,
|
||||
fileUrl: '/files/business-license.pdf',
|
||||
fileName: 'Business-License-2026.pdf',
|
||||
uploadDate: '2026-01-10',
|
||||
@@ -44,8 +44,8 @@ export const mockCertificates: Certificate[] = [
|
||||
{
|
||||
id: '3',
|
||||
name: 'FCC认证',
|
||||
type: 'SAFETY_CERT',
|
||||
status: 'PENDING',
|
||||
type: CertificateType.SAFETY_CERT,
|
||||
status: CertificateStatus.PENDING,
|
||||
fileUrl: '/files/fcc-cert.pdf',
|
||||
fileName: 'FCC-Certificate.pdf',
|
||||
uploadDate: '2026-03-18',
|
||||
@@ -57,8 +57,8 @@ export const mockCertificates: Certificate[] = [
|
||||
{
|
||||
id: '4',
|
||||
name: 'ISO9001',
|
||||
type: 'QUALITY_CERT',
|
||||
status: 'EXPIRED',
|
||||
type: CertificateType.QUALITY_CERT,
|
||||
status: CertificateStatus.EXPIRED,
|
||||
fileUrl: '/files/iso9001.pdf',
|
||||
fileName: 'ISO9001-2025.pdf',
|
||||
uploadDate: '2025-01-01',
|
||||
@@ -70,8 +70,8 @@ export const mockCertificates: Certificate[] = [
|
||||
{
|
||||
id: '5',
|
||||
name: 'RoHS认证',
|
||||
type: 'PRODUCT_CERT',
|
||||
status: 'APPROVED',
|
||||
type: CertificateType.PRODUCT_CERT,
|
||||
status: CertificateStatus.APPROVED,
|
||||
fileUrl: '/files/rohs-cert.pdf',
|
||||
fileName: 'RoHS-Certificate.pdf',
|
||||
uploadDate: '2026-02-20',
|
||||
@@ -85,8 +85,8 @@ export const mockCertificates: Certificate[] = [
|
||||
{
|
||||
id: '6',
|
||||
name: 'UL认证',
|
||||
type: 'SAFETY_CERT',
|
||||
status: 'PENDING',
|
||||
type: CertificateType.SAFETY_CERT,
|
||||
status: CertificateStatus.PENDING,
|
||||
fileUrl: '/files/ul-cert.pdf',
|
||||
fileName: 'UL-Certificate.pdf',
|
||||
uploadDate: '2026-03-19',
|
||||
@@ -98,8 +98,8 @@ export const mockCertificates: Certificate[] = [
|
||||
{
|
||||
id: '7',
|
||||
name: 'CCC认证',
|
||||
type: 'PRODUCT_CERT',
|
||||
status: 'APPROVED',
|
||||
type: CertificateType.PRODUCT_CERT,
|
||||
status: CertificateStatus.APPROVED,
|
||||
fileUrl: '/files/ccc-cert.pdf',
|
||||
fileName: 'CCC-Certificate.pdf',
|
||||
uploadDate: '2026-01-15',
|
||||
@@ -113,8 +113,8 @@ export const mockCertificates: Certificate[] = [
|
||||
{
|
||||
id: '8',
|
||||
name: 'GS认证',
|
||||
type: 'SAFETY_CERT',
|
||||
status: 'REJECTED',
|
||||
type: CertificateType.SAFETY_CERT,
|
||||
status: CertificateStatus.REJECTED,
|
||||
fileUrl: '/files/gs-cert.pdf',
|
||||
fileName: 'GS-Certificate.pdf',
|
||||
uploadDate: '2026-03-10',
|
||||
@@ -176,8 +176,8 @@ export function createMockCertificate(data: Partial<Certificate>): Certificate {
|
||||
const newCert: Certificate = {
|
||||
id: `${Date.now()}`,
|
||||
name: data.name || '',
|
||||
type: data.type || 'OTHER',
|
||||
status: 'PENDING',
|
||||
type: data.type || CertificateType.OTHER,
|
||||
status: CertificateStatus.PENDING,
|
||||
fileUrl: data.fileUrl || '/files/uploaded.pdf',
|
||||
fileName: data.fileName || 'uploaded.pdf',
|
||||
uploadDate: new Date().toISOString().split('T')[0],
|
||||
|
||||
@@ -28,6 +28,7 @@ export interface SelectionRule {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
category?: string;
|
||||
criteria: any;
|
||||
enabled: boolean;
|
||||
selected_count: number;
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
updateMockCertificate,
|
||||
deleteMockCertificate,
|
||||
} from './data/certificate.mock';
|
||||
import type { Certificate } from '@/types/certificate';
|
||||
|
||||
/**
|
||||
* MSW请求处理器
|
||||
@@ -79,7 +80,7 @@ export const handlers = [
|
||||
// 创建证书
|
||||
// ============================================
|
||||
http.post('/api/v1/certificate/certificates', async ({ request }) => {
|
||||
const body = await request.json();
|
||||
const body = await request.json() as Partial<Certificate>;
|
||||
|
||||
console.log('[MSW] POST /api/v1/certificate/certificates', body);
|
||||
|
||||
@@ -99,7 +100,7 @@ export const handlers = [
|
||||
// ============================================
|
||||
http.put('/api/v1/certificate/certificates/:id', async ({ params, request }) => {
|
||||
const { id } = params;
|
||||
const body = await request.json();
|
||||
const body = await request.json() as Partial<Certificate>;
|
||||
|
||||
console.log('[MSW] PUT /api/v1/certificate/certificates/:id', { id, body });
|
||||
|
||||
@@ -123,11 +124,12 @@ export const handlers = [
|
||||
// ============================================
|
||||
http.put('/api/v1/certificate/certificates/:id/status', async ({ params, request }) => {
|
||||
const { id } = params;
|
||||
const { status, approvedBy } = await request.json();
|
||||
const body = await request.json() as { status: string; approvedBy?: string };
|
||||
const { status, approvedBy } = body;
|
||||
|
||||
console.log('[MSW] PUT /api/v1/certificate/certificates/:id/status', { id, status, approvedBy });
|
||||
|
||||
const updates: any = { status };
|
||||
const updates: Partial<Certificate> = { status: status as Certificate['status'] };
|
||||
if (status === 'APPROVED') {
|
||||
updates.approvedBy = approvedBy || 'admin';
|
||||
updates.approvedDate = new Date().toISOString().split('T')[0];
|
||||
|
||||
@@ -174,7 +174,7 @@ export const ABTestConfigPage: React.FC = () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
const config: ABTestConfig = {
|
||||
const config: ABTestConfigData = {
|
||||
...values,
|
||||
variants,
|
||||
};
|
||||
|
||||
@@ -287,7 +287,12 @@ const AIDecisionLogPage: React.FC = () => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [detailVisible, setDetailVisible] = useState(false);
|
||||
const [selectedLog, setSelectedLog] = useState<any>(null);
|
||||
const [filters, setFilters] = useState({
|
||||
const [filters, setFilters] = useState<{
|
||||
decision_type: string | undefined;
|
||||
status: string | undefined;
|
||||
date_range: [any, any] | null;
|
||||
search: string;
|
||||
}>({
|
||||
decision_type: undefined,
|
||||
status: undefined,
|
||||
date_range: null,
|
||||
@@ -412,7 +417,7 @@ const AIDecisionLogPage: React.FC = () => {
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 150,
|
||||
fixed: 'right',
|
||||
fixed: 'right' as const,
|
||||
render: (_: any, record: any) => (
|
||||
<Space>
|
||||
<Button
|
||||
@@ -625,7 +630,11 @@ const AIDecisionLogPage: React.FC = () => {
|
||||
showQuickJumper: true,
|
||||
showTotal: total => `共 ${total} 条记录`,
|
||||
}}
|
||||
onChange={setPagination}
|
||||
onChange={(pag) => setPagination({
|
||||
current: pag.current || 1,
|
||||
pageSize: pag.pageSize || 10,
|
||||
total: pag.total || 0,
|
||||
})}
|
||||
scroll={{ x: 1500 }}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Card, Typography, Row, Col, Form, Input, Select, Button, Table, Statistic, Spin, message, Alert } from 'antd';
|
||||
import { LineChart, Line, BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, PieChart, Pie, Cell } from 'recharts';
|
||||
import { RocketOutlined, SaveOutlined, BarChartOutlined, TargetOutlined, ReloadOutlined } from '@ant-design/icons';
|
||||
import { RocketOutlined, SaveOutlined, BarChartOutlined, AimOutlined, ReloadOutlined } from '@ant-design/icons';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { adOptimizationDataSource, OptimizationSuggestion, AdPerformance } from '@/services/adOptimizationDataSource';
|
||||
|
||||
|
||||
@@ -259,8 +259,8 @@ export const AdDelivery: React.FC = () => {
|
||||
return (spend / clicks).toFixed(2);
|
||||
};
|
||||
|
||||
const calculateROAS = (sales: number, spend: number) => {
|
||||
if (spend === 0) return 0;
|
||||
const calculateROAS = (sales: number, spend: number): string => {
|
||||
if (spend === 0) return '0.00';
|
||||
return (sales / spend).toFixed(2);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Card, Typography, Row, Col, Form, Input, Select, Button, Table, Switch, DatePicker, TimePicker, message, Alert, Modal } from 'antd';
|
||||
import { PlusOutlined, EditOutlined, DeleteOutlined, SaveOutlined, PlayCircleOutlined, PauseCircleOutlined, HistoryOutlined } from '@ant-design/icons';
|
||||
import { Link } from 'umi';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { marketingDataSource, Ad } from '@/services/marketingDataSource';
|
||||
|
||||
const { Title, Text, Paragraph } = Typography;
|
||||
@@ -49,7 +49,7 @@ const AutoAdjustment: React.FC = () => {
|
||||
type: 'ROI',
|
||||
condition: 'ROI < 3.0',
|
||||
action: '降低CPC 10%',
|
||||
status: 'active',
|
||||
status: 'active' as const,
|
||||
createdAt: '2026-03-10',
|
||||
lastExecuted: '2026-03-18 10:30',
|
||||
executionCount: 15,
|
||||
@@ -62,7 +62,7 @@ const AutoAdjustment: React.FC = () => {
|
||||
type: 'CTR',
|
||||
condition: 'CTR < 2.0%',
|
||||
action: '调整创意',
|
||||
status: 'paused',
|
||||
status: 'paused' as const,
|
||||
createdAt: '2026-03-08',
|
||||
lastExecuted: '2026-03-15 14:20',
|
||||
executionCount: 8,
|
||||
@@ -75,7 +75,7 @@ const AutoAdjustment: React.FC = () => {
|
||||
type: 'Budget',
|
||||
condition: '日花费 < 预算的80%',
|
||||
action: '增加预算10%',
|
||||
status: 'active',
|
||||
status: 'active' as const,
|
||||
createdAt: '2026-03-05',
|
||||
lastExecuted: '2026-03-18 09:15',
|
||||
executionCount: 20,
|
||||
@@ -152,7 +152,7 @@ const AutoAdjustment: React.FC = () => {
|
||||
const newStrategy: Strategy = {
|
||||
id: (strategies.length + 1).toString(),
|
||||
...values,
|
||||
status: 'inactive' as const,
|
||||
status: (values.status || 'inactive') as 'active' | 'inactive' | 'paused',
|
||||
createdAt: new Date().toISOString().split('T')[0],
|
||||
lastExecuted: '-',
|
||||
executionCount: 0,
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react';
|
||||
import { Card, Typography, Row, Col, Select, Button, Table, Statistic, Spin, message, Alert, Badge, Dropdown, Menu } from 'antd';
|
||||
import { LineChart, Line, BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, PieChart, Pie, Cell } from 'recharts';
|
||||
import { ReloadOutlined, ExportOutlined, FilterOutlined, BellOutlined, EyeOutlined } from '@ant-design/icons';
|
||||
import { Link } from 'umi';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { marketingDataSource, Ad } from '@/services/marketingDataSource';
|
||||
|
||||
const { Title, Text, Paragraph } = Typography;
|
||||
|
||||
@@ -30,30 +30,14 @@ import {
|
||||
AuditOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
import { afterSalesDataSource, RefundRecord } from '@/services/afterSalesDataSource';
|
||||
import { afterSalesDataSource } from '@/services/afterSalesDataSource';
|
||||
import type { RefundRecord as RefundRecordType } from '@/services/afterSalesDataSource';
|
||||
|
||||
const { TextArea } = Input;
|
||||
const { Option } = Select;
|
||||
const { TabPane } = Tabs;
|
||||
|
||||
interface RefundRecord {
|
||||
id: string;
|
||||
orderId: string;
|
||||
refundId: string;
|
||||
customerId: string;
|
||||
customerName: string;
|
||||
amount: number;
|
||||
currency: string;
|
||||
reason: string;
|
||||
status: 'PENDING' | 'APPROVED' | 'REJECTED' | 'PROCESSING' | 'COMPLETED' | 'FAILED';
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
platform: string;
|
||||
refundMethod: string;
|
||||
transactionId?: string;
|
||||
processedBy?: string;
|
||||
remark?: string;
|
||||
}
|
||||
type RefundRecord = RefundRecordType;
|
||||
|
||||
const REFUND_STATUS_MAP: Record<string, { color: string; text: string; icon: React.ReactNode }> = {
|
||||
PENDING: { color: 'warning', text: 'Pending', icon: <ClockCircleOutlined /> },
|
||||
|
||||
@@ -42,8 +42,8 @@ import {
|
||||
LineChartOutlined,
|
||||
PieChartOutlined,
|
||||
DollarOutlined,
|
||||
TrendingUpOutlined,
|
||||
TrendingDownOutlined,
|
||||
RiseOutlined,
|
||||
FallOutlined,
|
||||
DownloadOutlined,
|
||||
SearchOutlined,
|
||||
FilterOutlined,
|
||||
@@ -55,8 +55,6 @@ import {
|
||||
ShopOutlined,
|
||||
ProductOutlined,
|
||||
OrderedListOutlined,
|
||||
RiseOutlined,
|
||||
FallOutlined,
|
||||
EyeOutlined,
|
||||
EditOutlined,
|
||||
DeleteOutlined,
|
||||
@@ -361,7 +359,7 @@ export const Analytics: React.FC = () => {
|
||||
<div>
|
||||
<div style={{ fontWeight: 500 }}>{text}</div>
|
||||
<div style={{ fontSize: 12, color: '#999' }}>{record.productId}</div>
|
||||
<Tag size="small">{record.category}</Tag>
|
||||
<Tag style={{ fontSize: 11 }}>{record.category}</Tag>
|
||||
</div>
|
||||
</Space>
|
||||
),
|
||||
@@ -723,7 +721,7 @@ export const Analytics: React.FC = () => {
|
||||
<Descriptions bordered column={1}>
|
||||
{Object.entries(selectedItem as object).map(([key, value]) => (
|
||||
<Descriptions.Item key={key} label={key}>
|
||||
{typeof value === 'object' ? JSON.stringify(value) : String(value)}
|
||||
{typeof value === 'object' ? JSON.stringify(value) : String(value ?? '')}
|
||||
</Descriptions.Item>
|
||||
))}
|
||||
</Descriptions>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useNavigate } from 'umi';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
Card,
|
||||
Form,
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from './index.tsx';
|
||||
@@ -62,6 +62,7 @@ import {
|
||||
AlertOutlined,
|
||||
InfoCircleOutlined,
|
||||
PlusOutlined,
|
||||
RiseOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
import moment from 'moment';
|
||||
@@ -193,15 +194,19 @@ const AutoProductSelection: React.FC = () => {
|
||||
const values = await ruleForm.validateFields();
|
||||
await productSelectionDataSource.createSelectionRule({
|
||||
name: values.name,
|
||||
description: values.description || '',
|
||||
category: values.category,
|
||||
min_roi: values.minROI,
|
||||
max_roi: values.maxROI,
|
||||
min_profit: values.minProfit,
|
||||
max_price: values.maxPrice,
|
||||
min_sales_volume: values.minSalesVolume,
|
||||
max_competition_level: values.maxCompetitionLevel,
|
||||
min_rating: values.minRating,
|
||||
trend_filter: values.trendFilter,
|
||||
criteria: {
|
||||
min_roi: values.minROI,
|
||||
max_roi: values.maxROI,
|
||||
min_profit: values.minProfit,
|
||||
max_price: values.maxPrice,
|
||||
min_sales_volume: values.minSalesVolume,
|
||||
max_competition_level: values.maxCompetitionLevel,
|
||||
min_rating: values.minRating,
|
||||
trend_filter: values.trendFilter,
|
||||
},
|
||||
enabled: true,
|
||||
});
|
||||
setRuleModalVisible(false);
|
||||
ruleForm.resetFields();
|
||||
@@ -335,7 +340,7 @@ const AutoProductSelection: React.FC = () => {
|
||||
<div style={{ fontWeight: 500 }}>{text}</div>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>SKU: {record.product_id}</Text>
|
||||
<div>
|
||||
{record.tags.map(tag => (
|
||||
{(record.tags || []).map(tag => (
|
||||
<Tag key={tag} color="blue" style={{ fontSize: 10 }}>{tag}</Tag>
|
||||
))}
|
||||
</div>
|
||||
@@ -355,8 +360,8 @@ const AutoProductSelection: React.FC = () => {
|
||||
width: 150,
|
||||
render: (_, record) => (
|
||||
<div>
|
||||
<div>¥{record.price.toFixed(2)}</div>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>成本: ¥{record.cost_price.toFixed(2)}</Text>
|
||||
<div>¥{(record.price ?? 0).toFixed(2)}</div>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>成本: ¥{(record.cost_price ?? 0).toFixed(2)}</Text>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
@@ -366,7 +371,7 @@ const AutoProductSelection: React.FC = () => {
|
||||
width: 120,
|
||||
render: (_, record) => (
|
||||
<div>
|
||||
<div style={{ color: '#52c41a', fontWeight: 500 }}>¥{record.profit.toFixed(2)}</div>
|
||||
<div style={{ color: '#52c41a', fontWeight: 500 }}>¥{(record.profit ?? 0).toFixed(2)}</div>
|
||||
<Tag color="green" style={{ fontSize: 11 }}>{record.roi.toFixed(0)}% ROI</Tag>
|
||||
</div>
|
||||
),
|
||||
@@ -377,7 +382,7 @@ const AutoProductSelection: React.FC = () => {
|
||||
width: 120,
|
||||
render: (_, record) => (
|
||||
<div>
|
||||
<div>{record.sales_volume.toLocaleString()} 销量</div>
|
||||
<div>{(record.sales_volume ?? 0).toLocaleString()} 销量</div>
|
||||
<Space>
|
||||
<StarOutlined style={{ color: '#faad14', fontSize: 12 }} />
|
||||
<Text style={{ fontSize: 12 }}>{record.rating} ({record.review_count})</Text>
|
||||
@@ -455,7 +460,7 @@ const AutoProductSelection: React.FC = () => {
|
||||
title: 'ROI范围',
|
||||
key: 'roi',
|
||||
width: 120,
|
||||
render: (_, record) => `${record.min_roi}% - ${record.max_roi}%`,
|
||||
render: (_, record) => `${record.criteria?.min_roi ?? 0}% - ${record.criteria?.max_roi ?? 0}%`,
|
||||
},
|
||||
{
|
||||
title: '利润要求',
|
||||
@@ -669,7 +674,7 @@ const AutoProductSelection: React.FC = () => {
|
||||
title="平均ROI"
|
||||
value={selectionStats?.avgROI.toFixed(1) || 0}
|
||||
suffix="%"
|
||||
prefix={<TrendingUpOutlined />}
|
||||
prefix={<RiseOutlined />}
|
||||
valueStyle={{ color: '#52c41a' }}
|
||||
/>
|
||||
</Col>
|
||||
@@ -1153,7 +1158,7 @@ const AutoProductSelection: React.FC = () => {
|
||||
<div>
|
||||
<Text strong>标签: </Text>
|
||||
<Space wrap>
|
||||
{selectedProduct.tags.map(tag => <Tag key={tag} color="blue">{tag}</Tag>)}
|
||||
{(selectedProduct.tags || []).map(tag => <Tag key={tag} color="blue">{tag}</Tag>)}
|
||||
</Space>
|
||||
</div>
|
||||
</Space>
|
||||
|
||||
@@ -98,11 +98,16 @@ export const BatchOrder: React.FC = () => {
|
||||
const mockOrders: B2BBatchOrder[] = [
|
||||
{
|
||||
id: 'BO-2026-001',
|
||||
batchId: 'BO-2026-001',
|
||||
customerId: 'C001',
|
||||
customerName: 'TechCorp Industries',
|
||||
totalItems: 2,
|
||||
validItems: 2,
|
||||
invalidItems: 0,
|
||||
currency: 'USD',
|
||||
items: [
|
||||
{ key: '1', productId: 'P001', productName: 'Industrial Sensor A', quantity: 50, unitPrice: 142.50, totalPrice: 7125 },
|
||||
{ key: '2', productId: 'P002', productName: 'Control Module B', quantity: 20, unitPrice: 266.00, totalPrice: 5320 },
|
||||
{ key: '1', lineNumber: 1, productId: 'P001', sku: 'SKU-P001', productName: 'Industrial Sensor A', quantity: 50, unitPrice: 142.50, totalPrice: 7125, customerId: 'C001', customerName: 'TechCorp Industries', status: 'VALID' },
|
||||
{ key: '2', lineNumber: 2, productId: 'P002', sku: 'SKU-P002', productName: 'Control Module B', quantity: 20, unitPrice: 266.00, totalPrice: 5320, customerId: 'C001', customerName: 'TechCorp Industries', status: 'VALID' },
|
||||
],
|
||||
totalAmount: 12445,
|
||||
status: 'PROCESSING',
|
||||
@@ -111,10 +116,15 @@ export const BatchOrder: React.FC = () => {
|
||||
},
|
||||
{
|
||||
id: 'BO-2026-002',
|
||||
batchId: 'BO-2026-002',
|
||||
customerId: 'C002',
|
||||
customerName: 'Global Manufacturing Co',
|
||||
totalItems: 1,
|
||||
validItems: 1,
|
||||
invalidItems: 0,
|
||||
currency: 'USD',
|
||||
items: [
|
||||
{ key: '1', productId: 'P003', productName: 'Power Supply Unit C', quantity: 100, unitPrice: 85.50, totalPrice: 8550 },
|
||||
{ key: '1', lineNumber: 1, productId: 'P003', sku: 'SKU-P003', productName: 'Power Supply Unit C', quantity: 100, unitPrice: 85.50, totalPrice: 8550, customerId: 'C002', customerName: 'Global Manufacturing Co', status: 'VALID' },
|
||||
],
|
||||
totalAmount: 8550,
|
||||
status: 'CONFIRMED',
|
||||
@@ -123,11 +133,16 @@ export const BatchOrder: React.FC = () => {
|
||||
},
|
||||
{
|
||||
id: 'BO-2026-003',
|
||||
batchId: 'BO-2026-003',
|
||||
customerId: 'C003',
|
||||
customerName: 'Smart Factory Solutions',
|
||||
totalItems: 2,
|
||||
validItems: 2,
|
||||
invalidItems: 0,
|
||||
currency: 'USD',
|
||||
items: [
|
||||
{ key: '1', productId: 'P004', productName: 'Communication Gateway D', quantity: 30, unitPrice: 399.00, totalPrice: 11970 },
|
||||
{ key: '2', productId: 'P005', productName: 'Display Panel E', quantity: 50, unitPrice: 175.75, totalPrice: 8787.50 },
|
||||
{ key: '1', lineNumber: 1, productId: 'P004', sku: 'SKU-P004', productName: 'Communication Gateway D', quantity: 30, unitPrice: 399.00, totalPrice: 11970, customerId: 'C003', customerName: 'Smart Factory Solutions', status: 'VALID' },
|
||||
{ key: '2', lineNumber: 2, productId: 'P005', sku: 'SKU-P005', productName: 'Display Panel E', quantity: 50, unitPrice: 175.75, totalPrice: 8787.50, customerId: 'C003', customerName: 'Smart Factory Solutions', status: 'VALID' },
|
||||
],
|
||||
totalAmount: 20757.50,
|
||||
status: 'COMPLETED',
|
||||
@@ -192,12 +207,16 @@ export const BatchOrder: React.FC = () => {
|
||||
try {
|
||||
const newOrder: B2BBatchOrder = {
|
||||
id: `BO-${Date.now()}`,
|
||||
batchId: `BATCH-${Date.now()}`,
|
||||
customerId: selectedCustomer.id,
|
||||
customerName: selectedCustomer.name,
|
||||
items: orderItems,
|
||||
totalItems: orderItems.length,
|
||||
totalAmount: orderItems.reduce((sum, item) => sum + item.totalPrice, 0),
|
||||
currency: 'USD',
|
||||
status: 'DRAFT',
|
||||
createdAt: new Date().toISOString(),
|
||||
validItems: orderItems.length,
|
||||
invalidItems: 0,
|
||||
};
|
||||
|
||||
setOrders([newOrder, ...orders]);
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
Modal,
|
||||
Form,
|
||||
Input,
|
||||
InputNumber,
|
||||
Select,
|
||||
DatePicker,
|
||||
Descriptions,
|
||||
@@ -96,7 +97,7 @@ export const ContractManage: React.FC = () => {
|
||||
const mockContracts: Contract[] = [
|
||||
{
|
||||
id: '1',
|
||||
contractNo: 'CTR-2026-001',
|
||||
contractId: 'CTR-2026-001',
|
||||
customerId: 'C001',
|
||||
customerName: 'TechCorp Industries',
|
||||
title: 'Annual Supply Agreement 2026',
|
||||
@@ -105,6 +106,7 @@ export const ContractManage: React.FC = () => {
|
||||
startDate: '2026-01-01',
|
||||
endDate: '2026-12-31',
|
||||
totalAmount: 500000,
|
||||
currency: 'USD',
|
||||
signedDate: '2025-12-15',
|
||||
signedBy: 'John Smith',
|
||||
attachments: ['contract_ctr_2026_001.pdf'],
|
||||
@@ -114,7 +116,7 @@ export const ContractManage: React.FC = () => {
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
contractNo: 'CTR-2026-002',
|
||||
contractId: 'CTR-2026-002',
|
||||
customerId: 'C002',
|
||||
customerName: 'Global Manufacturing Co',
|
||||
title: 'OEM Manufacturing Agreement',
|
||||
@@ -123,6 +125,7 @@ export const ContractManage: React.FC = () => {
|
||||
startDate: '2026-03-01',
|
||||
endDate: '2027-02-28',
|
||||
totalAmount: 800000,
|
||||
currency: 'USD',
|
||||
attachments: ['oem_agreement_draft.pdf', 'specification_v2.pdf'],
|
||||
terms: 'Exclusive manufacturing rights for North America region. Quality standards: ISO 9001:2015. Lead time: 45 days.',
|
||||
createdAt: '2026-03-10 09:00:00',
|
||||
@@ -130,7 +133,7 @@ export const ContractManage: React.FC = () => {
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
contractNo: 'CTR-2025-015',
|
||||
contractId: 'CTR-2025-015',
|
||||
customerId: 'C003',
|
||||
customerName: 'Smart Factory Solutions',
|
||||
title: 'Distribution Partnership Agreement',
|
||||
@@ -139,6 +142,7 @@ export const ContractManage: React.FC = () => {
|
||||
startDate: '2025-01-01',
|
||||
endDate: '2025-12-31',
|
||||
totalAmount: 300000,
|
||||
currency: 'USD',
|
||||
signedDate: '2024-12-20',
|
||||
signedBy: 'Mike Johnson',
|
||||
attachments: ['distribution_2025.pdf'],
|
||||
@@ -149,7 +153,7 @@ export const ContractManage: React.FC = () => {
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
contractNo: 'CTR-2026-003',
|
||||
contractId: 'CTR-2026-003',
|
||||
customerId: 'C001',
|
||||
customerName: 'TechCorp Industries',
|
||||
title: 'Service Level Agreement',
|
||||
@@ -158,6 +162,7 @@ export const ContractManage: React.FC = () => {
|
||||
startDate: '2026-04-01',
|
||||
endDate: '2027-03-31',
|
||||
totalAmount: 120000,
|
||||
currency: 'USD',
|
||||
attachments: [],
|
||||
terms: '24/7 technical support. Response time: 4 hours for critical issues. On-site support included.',
|
||||
createdAt: '2026-03-18 10:30:00',
|
||||
@@ -176,7 +181,7 @@ export const ContractManage: React.FC = () => {
|
||||
const customer = MOCK_CUSTOMERS.find(c => c.id === values.customerId);
|
||||
const newContract: Contract = {
|
||||
id: `${Date.now()}`,
|
||||
contractNo: `CTR-${new Date().getFullYear()}-${String(contracts.length + 1).padStart(3, '0')}`,
|
||||
contractId: `CTR-${new Date().getFullYear()}-${String(contracts.length + 1).padStart(3, '0')}`,
|
||||
customerId: values.customerId,
|
||||
customerName: customer?.name || '',
|
||||
title: values.title,
|
||||
@@ -185,6 +190,7 @@ export const ContractManage: React.FC = () => {
|
||||
startDate: values.dateRange[0].format('YYYY-MM-DD'),
|
||||
endDate: values.dateRange[1].format('YYYY-MM-DD'),
|
||||
totalAmount: values.totalAmount,
|
||||
currency: 'USD',
|
||||
attachments: [],
|
||||
terms: values.terms,
|
||||
createdAt: new Date().toISOString(),
|
||||
@@ -252,8 +258,8 @@ export const ContractManage: React.FC = () => {
|
||||
const columns: ColumnsType<Contract> = [
|
||||
{
|
||||
title: 'Contract No',
|
||||
dataIndex: 'contractNo',
|
||||
key: 'contractNo',
|
||||
dataIndex: 'contractId',
|
||||
key: 'contractId',
|
||||
width: 140,
|
||||
render: (no: string, record: Contract) => (
|
||||
<a onClick={() => handleViewDetail(record)}>{no}</a>
|
||||
@@ -468,7 +474,7 @@ export const ContractManage: React.FC = () => {
|
||||
{selectedContract && (
|
||||
<>
|
||||
<Descriptions bordered column={2}>
|
||||
<Descriptions.Item label="Contract No">{selectedContract.contractNo}</Descriptions.Item>
|
||||
<Descriptions.Item label="Contract No">{selectedContract.contractId}</Descriptions.Item>
|
||||
<Descriptions.Item label="Status">
|
||||
<Tag color={STATUS_CONFIG[selectedContract.status].color}>
|
||||
{STATUS_CONFIG[selectedContract.status].text}
|
||||
@@ -505,11 +511,11 @@ export const ContractManage: React.FC = () => {
|
||||
]}
|
||||
/>
|
||||
|
||||
{selectedContract.attachments.length > 0 && (
|
||||
{selectedContract.attachments && selectedContract.attachments.length > 0 && (
|
||||
<>
|
||||
<Divider>Attachments</Divider>
|
||||
<Space>
|
||||
{selectedContract.attachments.map((file, index) => (
|
||||
{selectedContract.attachments.map((file: string, index: number) => (
|
||||
<Button key={index} icon={<DownloadOutlined />} size="small">
|
||||
{file}
|
||||
</Button>
|
||||
|
||||
@@ -127,7 +127,7 @@ export const B2BTradeBatchOrder: React.FC = () => {
|
||||
|
||||
const parseUploadedFile = async (file: any) => {
|
||||
try {
|
||||
const items = await b2bTradeDataSource.parseOrderFile(file, selectedCustomer);
|
||||
const items = await b2bTradeDataSource.parseUploadFile(file, selectedCustomer?.id || '');
|
||||
setOrderItems(items);
|
||||
setCurrentStep(1);
|
||||
message.success('File parsed successfully');
|
||||
@@ -143,9 +143,11 @@ export const B2BTradeBatchOrder: React.FC = () => {
|
||||
const handleValidateItems = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const result = await b2bTradeDataSource.validateOrderItems(orderItems);
|
||||
setOrderItems(result.items);
|
||||
message.success(`Validation complete: ${result.validCount} valid, ${result.invalidCount} invalid`);
|
||||
const result = await b2bTradeDataSource.validateItems(orderItems);
|
||||
setOrderItems(result);
|
||||
const validCount = result.filter(i => i.status === 'VALID').length;
|
||||
const invalidCount = result.filter(i => i.status === 'INVALID').length;
|
||||
message.success(`Validation complete: ${validCount} valid, ${invalidCount} invalid`);
|
||||
} catch (error) {
|
||||
message.error('Validation failed');
|
||||
} finally {
|
||||
@@ -163,10 +165,9 @@ export const B2BTradeBatchOrder: React.FC = () => {
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
await b2bTradeDataSource.submitBatchOrder({
|
||||
await b2bTradeDataSource.createBatchOrder({
|
||||
customerId: values.customerId,
|
||||
items: validItems,
|
||||
note: values.note,
|
||||
});
|
||||
|
||||
message.success('Batch order submitted successfully');
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
Card,
|
||||
Form,
|
||||
Input,
|
||||
InputNumber,
|
||||
Select,
|
||||
Button,
|
||||
Table,
|
||||
@@ -132,6 +133,7 @@ export const ContractManage: React.FC = () => {
|
||||
status: 'ACTIVE',
|
||||
startDate: '2026-01-01',
|
||||
endDate: '2026-12-31',
|
||||
totalAmount: 500000.00,
|
||||
totalValue: 500000.00,
|
||||
currency: 'USD',
|
||||
signedDate: '2025-12-15',
|
||||
@@ -151,6 +153,7 @@ export const ContractManage: React.FC = () => {
|
||||
status: 'ACTIVE',
|
||||
startDate: '2026-02-01',
|
||||
endDate: '2028-01-31',
|
||||
totalAmount: 0,
|
||||
totalValue: 0,
|
||||
currency: 'USD',
|
||||
signedDate: '2026-01-20',
|
||||
@@ -167,9 +170,10 @@ export const ContractManage: React.FC = () => {
|
||||
customerName: 'Global Import Inc.',
|
||||
title: 'Purchase Agreement Q2',
|
||||
type: 'PURCHASE',
|
||||
status: 'PENDING_SIGN',
|
||||
status: 'PENDING_REVIEW',
|
||||
startDate: '2026-04-01',
|
||||
endDate: '2026-06-30',
|
||||
totalAmount: 150000.00,
|
||||
totalValue: 150000.00,
|
||||
currency: 'USD',
|
||||
attachments: ['purchase_agreement.pdf'],
|
||||
@@ -187,6 +191,7 @@ export const ContractManage: React.FC = () => {
|
||||
status: 'EXPIRED',
|
||||
startDate: '2025-01-01',
|
||||
endDate: '2025-12-31',
|
||||
totalAmount: 350000.00,
|
||||
totalValue: 350000.00,
|
||||
currency: 'USD',
|
||||
signedDate: '2025-01-05',
|
||||
@@ -661,7 +666,7 @@ export const ContractManage: React.FC = () => {
|
||||
</Tag>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="Value">
|
||||
{selectedContract.totalValue > 0 ? `$${selectedContract.totalValue.toLocaleString()}` : '-'}
|
||||
{selectedContract.totalValue != null ? `$${selectedContract.totalValue.toLocaleString()}` : '-'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="Start Date">{selectedContract.startDate}</Descriptions.Item>
|
||||
<Descriptions.Item label="End Date">{selectedContract.endDate}</Descriptions.Item>
|
||||
@@ -675,11 +680,11 @@ export const ContractManage: React.FC = () => {
|
||||
<Descriptions.Item label="Updated">{selectedContract.updatedAt}</Descriptions.Item>
|
||||
</Descriptions>
|
||||
|
||||
{selectedContract.attachments.length > 0 && (
|
||||
{selectedContract.attachments && selectedContract.attachments.length > 0 && (
|
||||
<>
|
||||
<Divider>Attachments</Divider>
|
||||
<Space>
|
||||
{selectedContract.attachments.map((file, index) => (
|
||||
{selectedContract.attachments.map((file: string, index: number) => (
|
||||
<Button
|
||||
key={index}
|
||||
icon={<DownloadOutlined />}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Link, useNavigate } from 'umi';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { Card, Button, Typography, Row, Col, Tag, Carousel, Pagination } from 'antd';
|
||||
import { ArrowRightOutlined, CheckCircleOutlined, DollarOutlined, BarChartOutlined, ZapOutlined, GlobeOutlined, StarOutlined, UserOutlined, CompanyOutlined, TrendingUpOutlined } from '@ant-design/icons';
|
||||
import Navbar from '@/components/Navbar';
|
||||
|
||||
86
dashboard/src/pages/Homepage.css
Normal file
86
dashboard/src/pages/Homepage.css
Normal file
@@ -0,0 +1,86 @@
|
||||
.homepage-button-primary {
|
||||
padding: 16px 40px;
|
||||
font-size: 18px;
|
||||
border-radius: 30px;
|
||||
box-shadow: 0 8px 24px rgba(24, 144, 255, 0.4);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.homepage-button-primary:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 12px 32px rgba(24, 144, 255, 0.5);
|
||||
}
|
||||
|
||||
.homepage-button-secondary {
|
||||
padding: 16px 40px;
|
||||
font-size: 18px;
|
||||
border-radius: 30px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: #fff;
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.homepage-button-secondary:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
transform: translateY(-3px);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.homepage-card {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.homepage-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.homepage-feature-card {
|
||||
transition: all 0.3s ease;
|
||||
border: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.homepage-feature-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.1);
|
||||
border-color: #1890ff;
|
||||
}
|
||||
|
||||
.homepage-pricing-card {
|
||||
transition: all 0.3s ease;
|
||||
border: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.homepage-pricing-card:hover {
|
||||
transform: scale(1.02);
|
||||
box-shadow: 0 16px 32px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.homepage-pricing-card-popular {
|
||||
border: 2px solid #1890ff;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.homepage-pricing-card-popular:hover {
|
||||
border-color: #40a9ff;
|
||||
}
|
||||
|
||||
.homepage-footer-link {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
.homepage-footer-link:hover {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.homepage-social-icon {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.homepage-social-icon:hover {
|
||||
color: #fff;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
@@ -22,27 +22,27 @@ const Homepage: React.FC = () => {
|
||||
const features = [
|
||||
{
|
||||
title: '智能选品系统',
|
||||
description: '基于AI的商品推荐,自动识别高潜力商品,提升选品效率',
|
||||
description: '基于AI的商品推荐,自动识别高潜力商品,提升选品效率'
|
||||
},
|
||||
{
|
||||
title: 'AI动态定价',
|
||||
description: '实时市场分析,优化定价策略,最大化利润',
|
||||
description: '实时市场分析,优化定价策略,最大化利润'
|
||||
},
|
||||
{
|
||||
title: '自动上架系统',
|
||||
description: '一键批量上架,节省人工成本,提高运营效率',
|
||||
description: '一键批量上架,节省人工成本,提高运营效率'
|
||||
},
|
||||
{
|
||||
title: '跨平台套利',
|
||||
description: '发现价格差异,实现利润最大化,降低运营风险',
|
||||
description: '发现价格差异,实现利润最大化,降低运营风险'
|
||||
},
|
||||
{
|
||||
title: 'AI店铺托管',
|
||||
description: '24/7智能运营,解放人力,提升店铺表现',
|
||||
description: '24/7智能运营,解放人力,提升店铺表现'
|
||||
},
|
||||
{
|
||||
title: '多商户管理',
|
||||
description: '集中管理多店铺,数据一目了然,简化管理流程',
|
||||
description: '集中管理多店铺,数据一目了然,简化管理流程'
|
||||
},
|
||||
];
|
||||
|
||||
@@ -130,12 +130,8 @@ const Homepage: React.FC = () => {
|
||||
fontSize: '18px',
|
||||
borderRadius: '30px',
|
||||
boxShadow: '0 8px 24px rgba(24, 144, 255, 0.4)',
|
||||
transition: 'all 0.3s ease',
|
||||
'&:hover': {
|
||||
transform: 'translateY(-3px)',
|
||||
boxShadow: '0 12px 32px rgba(24, 144, 255, 0.5)'
|
||||
}
|
||||
}}
|
||||
transition: 'all 0.3s ease'
|
||||
}}
|
||||
>
|
||||
免费注册
|
||||
</Button>
|
||||
@@ -149,12 +145,8 @@ const Homepage: React.FC = () => {
|
||||
background: 'rgba(255, 255, 255, 0.1)',
|
||||
color: '#fff',
|
||||
border: '1px solid rgba(255, 255, 255, 0.3)',
|
||||
transition: 'all 0.3s ease',
|
||||
'&:hover': {
|
||||
background: 'rgba(255, 255, 255, 0.2)',
|
||||
transform: 'translateY(-3px)'
|
||||
}
|
||||
}}
|
||||
transition: 'all 0.3s ease'
|
||||
}}
|
||||
>
|
||||
了解更多
|
||||
</Button>
|
||||
@@ -187,12 +179,8 @@ const Homepage: React.FC = () => {
|
||||
borderRadius: '16px',
|
||||
boxShadow: '0 20px 60px rgba(0, 0, 0, 0.4)',
|
||||
transition: 'all 0.3s ease',
|
||||
transformStyle: 'preserve-3d',
|
||||
'&:hover': {
|
||||
transform: 'scale(1.05) rotateY(5deg)',
|
||||
boxShadow: '0 24px 72px rgba(0, 0, 0, 0.5)'
|
||||
}
|
||||
}}
|
||||
transformStyle: 'preserve-3d'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Col>
|
||||
@@ -259,13 +247,8 @@ const Homepage: React.FC = () => {
|
||||
transition: 'all 0.4s ease',
|
||||
border: '1px solid #e8e8e8',
|
||||
boxShadow: '0 8px 24px rgba(0, 0, 0, 0.08)',
|
||||
height: '100%',
|
||||
'&:hover': {
|
||||
transform: 'translateY(-12px)',
|
||||
boxShadow: '0 16px 48px rgba(0, 0, 0, 0.15)',
|
||||
borderColor: '#1890ff'
|
||||
}
|
||||
}}
|
||||
height: '100%'
|
||||
}}
|
||||
>
|
||||
<div style={{ marginBottom: '32px', paddingTop: '32px' }}>
|
||||
<span style={{
|
||||
@@ -280,12 +263,8 @@ const Homepage: React.FC = () => {
|
||||
color: '#fff',
|
||||
borderRadius: '20px',
|
||||
boxShadow: '0 6px 20px rgba(24, 144, 255, 0.4)',
|
||||
transition: 'all 0.3s ease',
|
||||
'&:hover': {
|
||||
transform: 'scale(1.1)',
|
||||
boxShadow: '0 8px 24px rgba(24, 144, 255, 0.5)'
|
||||
}
|
||||
}}>{feature.title.charAt(0)}</span>
|
||||
transition: 'all 0.3s ease'
|
||||
}}>{feature.title.charAt(0)}</span>
|
||||
</div>
|
||||
<Title level={4} style={{
|
||||
marginBottom: '20px',
|
||||
@@ -308,12 +287,8 @@ const Homepage: React.FC = () => {
|
||||
alignItems: 'center',
|
||||
gap: '10px',
|
||||
transition: 'all 0.3s ease',
|
||||
fontSize: '16px',
|
||||
'&:hover': {
|
||||
color: '#40a9ff',
|
||||
transform: 'translateX(6px)'
|
||||
}
|
||||
}}
|
||||
fontSize: '16px'
|
||||
}}
|
||||
>
|
||||
了解更多 <ArrowRightOutlined />
|
||||
</Button>
|
||||
@@ -368,12 +343,8 @@ const Homepage: React.FC = () => {
|
||||
borderRadius: '16px',
|
||||
boxShadow: '0 20px 60px rgba(0, 0, 0, 0.15)',
|
||||
transition: 'all 0.4s ease',
|
||||
transformStyle: 'preserve-3d',
|
||||
'&:hover': {
|
||||
transform: 'scale(1.05) rotateY(-5deg)',
|
||||
boxShadow: '0 24px 72px rgba(0, 0, 0, 0.2)'
|
||||
}
|
||||
}}
|
||||
transformStyle: 'preserve-3d'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Col>
|
||||
@@ -407,13 +378,8 @@ const Homepage: React.FC = () => {
|
||||
padding: '24px',
|
||||
background: '#fafafa',
|
||||
borderRadius: '16px',
|
||||
transition: 'all 0.4s ease',
|
||||
'&:hover': {
|
||||
background: 'rgba(24, 144, 255, 0.05)',
|
||||
transform: 'translateX(12px)',
|
||||
boxShadow: '0 8px 24px rgba(0, 0, 0, 0.08)'
|
||||
}
|
||||
}}>
|
||||
transition: 'all 0.4s ease'
|
||||
}}>
|
||||
<div style={{
|
||||
width: '56px',
|
||||
height: '56px',
|
||||
@@ -450,13 +416,8 @@ const Homepage: React.FC = () => {
|
||||
padding: '24px',
|
||||
background: '#fafafa',
|
||||
borderRadius: '16px',
|
||||
transition: 'all 0.4s ease',
|
||||
'&:hover': {
|
||||
background: 'rgba(24, 144, 255, 0.05)',
|
||||
transform: 'translateX(12px)',
|
||||
boxShadow: '0 8px 24px rgba(0, 0, 0, 0.08)'
|
||||
}
|
||||
}}>
|
||||
transition: 'all 0.4s ease'
|
||||
}}>
|
||||
<div style={{
|
||||
width: '56px',
|
||||
height: '56px',
|
||||
@@ -493,13 +454,8 @@ const Homepage: React.FC = () => {
|
||||
padding: '24px',
|
||||
background: '#fafafa',
|
||||
borderRadius: '16px',
|
||||
transition: 'all 0.4s ease',
|
||||
'&:hover': {
|
||||
background: 'rgba(24, 144, 255, 0.05)',
|
||||
transform: 'translateX(12px)',
|
||||
boxShadow: '0 8px 24px rgba(0, 0, 0, 0.08)'
|
||||
}
|
||||
}}>
|
||||
transition: 'all 0.4s ease'
|
||||
}}>
|
||||
<div style={{
|
||||
width: '56px',
|
||||
height: '56px',
|
||||
@@ -536,13 +492,8 @@ const Homepage: React.FC = () => {
|
||||
padding: '24px',
|
||||
background: '#fafafa',
|
||||
borderRadius: '16px',
|
||||
transition: 'all 0.4s ease',
|
||||
'&:hover': {
|
||||
background: 'rgba(24, 144, 255, 0.05)',
|
||||
transform: 'translateX(12px)',
|
||||
boxShadow: '0 8px 24px rgba(0, 0, 0, 0.08)'
|
||||
}
|
||||
}}>
|
||||
transition: 'all 0.4s ease'
|
||||
}}>
|
||||
<div style={{
|
||||
width: '56px',
|
||||
height: '56px',
|
||||
@@ -635,13 +586,8 @@ const Homepage: React.FC = () => {
|
||||
transition: 'all 0.4s ease',
|
||||
border: '1px solid #e8e8e8',
|
||||
boxShadow: '0 12px 32px rgba(0, 0, 0, 0.1)',
|
||||
height: '100%',
|
||||
'&:hover': {
|
||||
transform: 'translateY(-16px)',
|
||||
boxShadow: '0 20px 60px rgba(0, 0, 0, 0.18)',
|
||||
borderColor: '#1890ff'
|
||||
}
|
||||
}}
|
||||
height: '100%'
|
||||
}}
|
||||
>
|
||||
<div style={{
|
||||
marginBottom: '0',
|
||||
@@ -656,11 +602,8 @@ const Homepage: React.FC = () => {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
objectFit: 'cover',
|
||||
transition: 'all 0.4s ease',
|
||||
'&:hover': {
|
||||
transform: 'scale(1.1)'
|
||||
}
|
||||
}}
|
||||
transition: 'all 0.4s ease'
|
||||
}}
|
||||
/>
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
@@ -708,12 +651,8 @@ const Homepage: React.FC = () => {
|
||||
alignItems: 'center',
|
||||
gap: '10px',
|
||||
transition: 'all 0.3s ease',
|
||||
fontSize: '16px',
|
||||
'&:hover': {
|
||||
color: '#40a9ff',
|
||||
transform: 'translateX(6px)'
|
||||
}
|
||||
}}
|
||||
fontSize: '16px'
|
||||
}}
|
||||
>
|
||||
查看详情 <ArrowRightOutlined />
|
||||
</Button>
|
||||
@@ -729,13 +668,8 @@ const Homepage: React.FC = () => {
|
||||
transition: 'all 0.4s ease',
|
||||
border: '1px solid #e8e8e8',
|
||||
boxShadow: '0 12px 32px rgba(0, 0, 0, 0.1)',
|
||||
height: '100%',
|
||||
'&:hover': {
|
||||
transform: 'translateY(-16px)',
|
||||
boxShadow: '0 20px 60px rgba(0, 0, 0, 0.18)',
|
||||
borderColor: '#1890ff'
|
||||
}
|
||||
}}
|
||||
height: '100%'
|
||||
}}
|
||||
>
|
||||
<div style={{
|
||||
marginBottom: '0',
|
||||
@@ -750,11 +684,8 @@ const Homepage: React.FC = () => {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
objectFit: 'cover',
|
||||
transition: 'all 0.4s ease',
|
||||
'&:hover': {
|
||||
transform: 'scale(1.1)'
|
||||
}
|
||||
}}
|
||||
transition: 'all 0.4s ease'
|
||||
}}
|
||||
/>
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
@@ -802,12 +733,8 @@ const Homepage: React.FC = () => {
|
||||
alignItems: 'center',
|
||||
gap: '10px',
|
||||
transition: 'all 0.3s ease',
|
||||
fontSize: '16px',
|
||||
'&:hover': {
|
||||
color: '#40a9ff',
|
||||
transform: 'translateX(6px)'
|
||||
}
|
||||
}}
|
||||
fontSize: '16px'
|
||||
}}
|
||||
>
|
||||
查看详情 <ArrowRightOutlined />
|
||||
</Button>
|
||||
@@ -823,13 +750,8 @@ const Homepage: React.FC = () => {
|
||||
transition: 'all 0.4s ease',
|
||||
border: '1px solid #e8e8e8',
|
||||
boxShadow: '0 12px 32px rgba(0, 0, 0, 0.1)',
|
||||
height: '100%',
|
||||
'&:hover': {
|
||||
transform: 'translateY(-16px)',
|
||||
boxShadow: '0 20px 60px rgba(0, 0, 0, 0.18)',
|
||||
borderColor: '#1890ff'
|
||||
}
|
||||
}}
|
||||
height: '100%'
|
||||
}}
|
||||
>
|
||||
<div style={{
|
||||
marginBottom: '0',
|
||||
@@ -844,11 +766,8 @@ const Homepage: React.FC = () => {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
objectFit: 'cover',
|
||||
transition: 'all 0.4s ease',
|
||||
'&:hover': {
|
||||
transform: 'scale(1.1)'
|
||||
}
|
||||
}}
|
||||
transition: 'all 0.4s ease'
|
||||
}}
|
||||
/>
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
@@ -896,12 +815,8 @@ const Homepage: React.FC = () => {
|
||||
alignItems: 'center',
|
||||
gap: '10px',
|
||||
transition: 'all 0.3s ease',
|
||||
fontSize: '16px',
|
||||
'&:hover': {
|
||||
color: '#40a9ff',
|
||||
transform: 'translateX(6px)'
|
||||
}
|
||||
}}
|
||||
fontSize: '16px'
|
||||
}}
|
||||
>
|
||||
查看详情 <ArrowRightOutlined />
|
||||
</Button>
|
||||
@@ -918,12 +833,8 @@ const Homepage: React.FC = () => {
|
||||
fontSize: '18px',
|
||||
borderRadius: '30px',
|
||||
boxShadow: '0 6px 18px rgba(24, 144, 255, 0.4)',
|
||||
transition: 'all 0.3s ease',
|
||||
'&:hover': {
|
||||
transform: 'translateY(-3px)',
|
||||
boxShadow: '0 8px 24px rgba(24, 144, 255, 0.5)'
|
||||
}
|
||||
}}
|
||||
transition: 'all 0.3s ease'
|
||||
}}
|
||||
>
|
||||
查看更多案例
|
||||
</Button>
|
||||
@@ -1000,12 +911,8 @@ const Homepage: React.FC = () => {
|
||||
fontSize: '20px',
|
||||
borderRadius: '35px',
|
||||
boxShadow: '0 8px 24px rgba(24, 144, 255, 0.4)',
|
||||
transition: 'all 0.3s ease',
|
||||
'&:hover': {
|
||||
transform: 'translateY(-3px)',
|
||||
boxShadow: '0 12px 32px rgba(24, 144, 255, 0.5)'
|
||||
}
|
||||
}}
|
||||
transition: 'all 0.3s ease'
|
||||
}}
|
||||
>
|
||||
免费注册
|
||||
</Button>
|
||||
@@ -1062,13 +969,8 @@ const Homepage: React.FC = () => {
|
||||
borderRadius: '50%',
|
||||
background: 'rgba(255, 255, 255, 0.08)',
|
||||
color: 'rgba(255, 255, 255, 0.6)',
|
||||
transition: 'all 0.3s ease',
|
||||
'&:hover': {
|
||||
background: '#1890ff',
|
||||
color: '#fff',
|
||||
transform: 'translateY(-2px)'
|
||||
}
|
||||
}}>
|
||||
transition: 'all 0.3s ease'
|
||||
}}>
|
||||
<TwitterOutlined />
|
||||
</a>
|
||||
<a href="#" style={{
|
||||
@@ -1080,13 +982,8 @@ const Homepage: React.FC = () => {
|
||||
borderRadius: '50%',
|
||||
background: 'rgba(255, 255, 255, 0.08)',
|
||||
color: 'rgba(255, 255, 255, 0.6)',
|
||||
transition: 'all 0.3s ease',
|
||||
'&:hover': {
|
||||
background: '#1890ff',
|
||||
color: '#fff',
|
||||
transform: 'translateY(-2px)'
|
||||
}
|
||||
}}>
|
||||
transition: 'all 0.3s ease'
|
||||
}}>
|
||||
<LinkedinOutlined />
|
||||
</a>
|
||||
<a href="#" style={{
|
||||
@@ -1098,13 +995,8 @@ const Homepage: React.FC = () => {
|
||||
borderRadius: '50%',
|
||||
background: 'rgba(255, 255, 255, 0.08)',
|
||||
color: 'rgba(255, 255, 255, 0.6)',
|
||||
transition: 'all 0.3s ease',
|
||||
'&:hover': {
|
||||
background: '#1890ff',
|
||||
color: '#fff',
|
||||
transform: 'translateY(-2px)'
|
||||
}
|
||||
}}>
|
||||
transition: 'all 0.3s ease'
|
||||
}}>
|
||||
<GithubOutlined />
|
||||
</a>
|
||||
</div>
|
||||
@@ -1122,42 +1014,26 @@ const Homepage: React.FC = () => {
|
||||
color: 'rgba(255, 255, 255, 0.6)',
|
||||
transition: 'all 0.3s ease',
|
||||
textDecoration: 'none',
|
||||
fontSize: '16px',
|
||||
'&:hover': {
|
||||
color: '#1890ff',
|
||||
transform: 'translateX(4px)'
|
||||
}
|
||||
}}>定价方案</Link></li>
|
||||
fontSize: '16px'
|
||||
}}>定价方案</Link></li>
|
||||
<li style={{ marginBottom: '20px' }}><Link to="/case-study" style={{
|
||||
color: 'rgba(255, 255, 255, 0.6)',
|
||||
transition: 'all 0.3s ease',
|
||||
textDecoration: 'none',
|
||||
fontSize: '16px',
|
||||
'&:hover': {
|
||||
color: '#1890ff',
|
||||
transform: 'translateX(4px)'
|
||||
}
|
||||
}}>成功案例</Link></li>
|
||||
fontSize: '16px'
|
||||
}}>成功案例</Link></li>
|
||||
<li style={{ marginBottom: '20px' }}><Link to="/" style={{
|
||||
color: 'rgba(255, 255, 255, 0.6)',
|
||||
transition: 'all 0.3s ease',
|
||||
textDecoration: 'none',
|
||||
fontSize: '16px',
|
||||
'&:hover': {
|
||||
color: '#1890ff',
|
||||
transform: 'translateX(4px)'
|
||||
}
|
||||
}}>功能介绍</Link></li>
|
||||
fontSize: '16px'
|
||||
}}>功能介绍</Link></li>
|
||||
<li style={{ marginBottom: '20px' }}><Link to="/" style={{
|
||||
color: 'rgba(255, 255, 255, 0.6)',
|
||||
transition: 'all 0.3s ease',
|
||||
textDecoration: 'none',
|
||||
fontSize: '16px',
|
||||
'&:hover': {
|
||||
color: '#1890ff',
|
||||
transform: 'translateX(4px)'
|
||||
}
|
||||
}}>API文档</Link></li>
|
||||
fontSize: '16px'
|
||||
}}>API文档</Link></li>
|
||||
</ul>
|
||||
</Col>
|
||||
<Col xs={24} md={6}>
|
||||
@@ -1173,42 +1049,26 @@ const Homepage: React.FC = () => {
|
||||
color: 'rgba(255, 255, 255, 0.6)',
|
||||
transition: 'all 0.3s ease',
|
||||
textDecoration: 'none',
|
||||
fontSize: '16px',
|
||||
'&:hover': {
|
||||
color: '#1890ff',
|
||||
transform: 'translateX(4px)'
|
||||
}
|
||||
}}>关于我们</Link></li>
|
||||
fontSize: '16px'
|
||||
}}>关于我们</Link></li>
|
||||
<li style={{ marginBottom: '20px' }}><Link to="/" style={{
|
||||
color: 'rgba(255, 255, 255, 0.6)',
|
||||
transition: 'all 0.3s ease',
|
||||
textDecoration: 'none',
|
||||
fontSize: '16px',
|
||||
'&:hover': {
|
||||
color: '#1890ff',
|
||||
transform: 'translateX(4px)'
|
||||
}
|
||||
}}>联系我们</Link></li>
|
||||
fontSize: '16px'
|
||||
}}>联系我们</Link></li>
|
||||
<li style={{ marginBottom: '20px' }}><Link to="/" style={{
|
||||
color: 'rgba(255, 255, 255, 0.6)',
|
||||
transition: 'all 0.3s ease',
|
||||
textDecoration: 'none',
|
||||
fontSize: '16px',
|
||||
'&:hover': {
|
||||
color: '#1890ff',
|
||||
transform: 'translateX(4px)'
|
||||
}
|
||||
}}>招贤纳士</Link></li>
|
||||
fontSize: '16px'
|
||||
}}>招贤纳士</Link></li>
|
||||
<li style={{ marginBottom: '20px' }}><Link to="/" style={{
|
||||
color: 'rgba(255, 255, 255, 0.6)',
|
||||
transition: 'all 0.3s ease',
|
||||
textDecoration: 'none',
|
||||
fontSize: '16px',
|
||||
'&:hover': {
|
||||
color: '#1890ff',
|
||||
transform: 'translateX(4px)'
|
||||
}
|
||||
}}>新闻资讯</Link></li>
|
||||
fontSize: '16px'
|
||||
}}>新闻资讯</Link></li>
|
||||
</ul>
|
||||
</Col>
|
||||
<Col xs={24} md={6}>
|
||||
@@ -1224,42 +1084,26 @@ const Homepage: React.FC = () => {
|
||||
color: 'rgba(255, 255, 255, 0.6)',
|
||||
transition: 'all 0.3s ease',
|
||||
textDecoration: 'none',
|
||||
fontSize: '16px',
|
||||
'&:hover': {
|
||||
color: '#1890ff',
|
||||
transform: 'translateX(4px)'
|
||||
}
|
||||
}}>帮助中心</Link></li>
|
||||
fontSize: '16px'
|
||||
}}>帮助中心</Link></li>
|
||||
<li style={{ marginBottom: '20px' }}><Link to="/" style={{
|
||||
color: 'rgba(255, 255, 255, 0.6)',
|
||||
transition: 'all 0.3s ease',
|
||||
textDecoration: 'none',
|
||||
fontSize: '16px',
|
||||
'&:hover': {
|
||||
color: '#1890ff',
|
||||
transform: 'translateX(4px)'
|
||||
}
|
||||
}}>常见问题</Link></li>
|
||||
fontSize: '16px'
|
||||
}}>常见问题</Link></li>
|
||||
<li style={{ marginBottom: '20px' }}><Link to="/" style={{
|
||||
color: 'rgba(255, 255, 255, 0.6)',
|
||||
transition: 'all 0.3s ease',
|
||||
textDecoration: 'none',
|
||||
fontSize: '16px',
|
||||
'&:hover': {
|
||||
color: '#1890ff',
|
||||
transform: 'translateX(4px)'
|
||||
}
|
||||
}}>联系支持</Link></li>
|
||||
fontSize: '16px'
|
||||
}}>联系支持</Link></li>
|
||||
<li style={{ marginBottom: '20px' }}><Link to="/" style={{
|
||||
color: 'rgba(255, 255, 255, 0.6)',
|
||||
transition: 'all 0.3s ease',
|
||||
textDecoration: 'none',
|
||||
fontSize: '16px',
|
||||
'&:hover': {
|
||||
color: '#1890ff',
|
||||
transform: 'translateX(4px)'
|
||||
}
|
||||
}}>API文档</Link></li>
|
||||
fontSize: '16px'
|
||||
}}>API文档</Link></li>
|
||||
</ul>
|
||||
</Col>
|
||||
</Row>
|
||||
@@ -1274,29 +1118,20 @@ const Homepage: React.FC = () => {
|
||||
color: 'rgba(255, 255, 255, 0.4)',
|
||||
transition: 'color 0.3s ease',
|
||||
textDecoration: 'none',
|
||||
margin: '0 16px',
|
||||
'&:hover': {
|
||||
color: '#1890ff'
|
||||
}
|
||||
}}>隐私政策</Link>
|
||||
margin: '0 16px'
|
||||
}}>隐私政策</Link>
|
||||
<Link to="/" style={{
|
||||
color: 'rgba(255, 255, 255, 0.4)',
|
||||
transition: 'color 0.3s ease',
|
||||
textDecoration: 'none',
|
||||
margin: '0 16px',
|
||||
'&:hover': {
|
||||
color: '#1890ff'
|
||||
}
|
||||
}}>服务条款</Link>
|
||||
margin: '0 16px'
|
||||
}}>服务条款</Link>
|
||||
<Link to="/" style={{
|
||||
color: 'rgba(255, 255, 255, 0.4)',
|
||||
transition: 'color 0.3s ease',
|
||||
textDecoration: 'none',
|
||||
margin: '0 16px',
|
||||
'&:hover': {
|
||||
color: '#1890ff'
|
||||
}
|
||||
}}>Cookie政策</Link>
|
||||
margin: '0 16px'
|
||||
}}>Cookie政策</Link>
|
||||
</div>
|
||||
© 2025 Crawlful Hub. All rights reserved.
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Card, Typography, Row, Col, Button, Statistic, Space, Tabs } from 'antd';
|
||||
import { FileTextOutlined, AlertOutlined, BarChartOutlined, SyncOutlined, PlusOutlined, SearchOutlined, FilterOutlined, DollarOutlined, TruckOutlined, ClockCircleOutlined, CheckCircleOutlined, CloseCircleOutlined } from '@ant-design/icons';
|
||||
import { Link } from 'umi';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Area, Pie, Column } from '@ant-design/plots';
|
||||
import moment from 'moment';
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Card, Typography, Row, Col, Button, Table, Input, Select, Modal, Form, InputNumber, message, Tag, Space, Popconfirm, Tabs } from 'antd';
|
||||
import { PlusOutlined, UploadOutlined, EditOutlined, SearchOutlined, FilterOutlined, SyncOutlined, DeleteOutlined, EyeOutlined, GlobalOutlined } from '@ant-design/icons';
|
||||
import { Link } from 'umi';
|
||||
import { Link } from 'react-router-dom';
|
||||
import CrossPlatformManage from './CrossPlatformManage';
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
|
||||
@@ -35,6 +35,62 @@ export interface ReturnApplication {
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface CustomerServiceTicket {
|
||||
id: string;
|
||||
ticketId: string;
|
||||
orderId: string;
|
||||
customerId: string;
|
||||
customerName: string;
|
||||
customerEmail: string;
|
||||
subject: string;
|
||||
category: string;
|
||||
priority: 'LOW' | 'MEDIUM' | 'HIGH' | 'URGENT';
|
||||
status: 'OPEN' | 'IN_PROGRESS' | 'WAITING_CUSTOMER' | 'RESOLVED' | 'CLOSED';
|
||||
assignedTo?: string;
|
||||
platform?: string;
|
||||
lastMessage?: string;
|
||||
messageCount?: number;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
messages?: CustomerServiceMessage[];
|
||||
}
|
||||
|
||||
export interface CustomerServiceMessage {
|
||||
id: string;
|
||||
ticketId: string;
|
||||
sender: 'customer' | 'agent';
|
||||
content: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface RefundRecord {
|
||||
id: string;
|
||||
orderId: string;
|
||||
refundId: string;
|
||||
customerId: string;
|
||||
customerName: string;
|
||||
amount: number;
|
||||
currency: string;
|
||||
reason: string;
|
||||
status: 'PENDING' | 'APPROVED' | 'REJECTED' | 'PROCESSING' | 'COMPLETED' | 'FAILED';
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
platform: string;
|
||||
refundMethod: string;
|
||||
transactionId?: string;
|
||||
processedBy?: string;
|
||||
remark?: string;
|
||||
}
|
||||
|
||||
export interface ReturnFormData {
|
||||
orderId: string;
|
||||
returnReason: string;
|
||||
returnDescription: string;
|
||||
returnItems: string[];
|
||||
images: string[];
|
||||
contactPhone: string;
|
||||
}
|
||||
|
||||
export interface AfterSalesQueryParams {
|
||||
orderId?: string;
|
||||
status?: string;
|
||||
@@ -49,9 +105,12 @@ export interface AfterSalesQueryParams {
|
||||
export interface IAfterSalesDataSource {
|
||||
fetchOrderItems(orderId: string): Promise<OrderItem[]>;
|
||||
submitReturn(data: Partial<ReturnApplication>): Promise<ReturnApplication>;
|
||||
submitReturnApply(data: ReturnFormData): Promise<ReturnApplication>;
|
||||
fetchReturnApplications(params?: AfterSalesQueryParams): Promise<ReturnApplication[]>;
|
||||
fetchReturnDetail(id: string): Promise<ReturnApplication | null>;
|
||||
updateReturnStatus(id: string, status: string): Promise<ReturnApplication>;
|
||||
fetchCustomerServiceTickets(params?: AfterSalesQueryParams): Promise<CustomerServiceTicket[]>;
|
||||
fetchRefundRecords(params?: AfterSalesQueryParams): Promise<RefundRecord[]>;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
@@ -75,8 +134,18 @@ class ApiAfterSalesDataSource implements IAfterSalesDataSource {
|
||||
return result.data;
|
||||
}
|
||||
|
||||
async submitReturnApply(data: ReturnFormData): Promise<ReturnApplication> {
|
||||
const response = await fetch('/api/v1/returns/apply', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
const result = await response.json();
|
||||
return result.data;
|
||||
}
|
||||
|
||||
async fetchReturnApplications(params?: AfterSalesQueryParams): Promise<ReturnApplication[]> {
|
||||
const query = new URLSearchParams(params as any).toString();
|
||||
const query = new URLSearchParams(params as Record<string, string>).toString();
|
||||
const response = await fetch(`/api/v1/returns?${query}`);
|
||||
const result = await response.json();
|
||||
return result.data;
|
||||
@@ -97,6 +166,20 @@ class ApiAfterSalesDataSource implements IAfterSalesDataSource {
|
||||
const result = await response.json();
|
||||
return result.data;
|
||||
}
|
||||
|
||||
async fetchCustomerServiceTickets(params?: AfterSalesQueryParams): Promise<CustomerServiceTicket[]> {
|
||||
const query = new URLSearchParams(params as Record<string, string>).toString();
|
||||
const response = await fetch(`/api/v1/customer-service/tickets?${query}`);
|
||||
const result = await response.json();
|
||||
return result.data;
|
||||
}
|
||||
|
||||
async fetchRefundRecords(params?: AfterSalesQueryParams): Promise<RefundRecord[]> {
|
||||
const query = new URLSearchParams(params as Record<string, string>).toString();
|
||||
const response = await fetch(`/api/v1/refunds?${query}`);
|
||||
const result = await response.json();
|
||||
return result.data;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
@@ -190,6 +273,64 @@ class MockAfterSalesDataSource implements IAfterSalesDataSource {
|
||||
return this.mockReturns[index];
|
||||
}
|
||||
|
||||
async submitReturnApply(data: ReturnFormData): Promise<ReturnApplication> {
|
||||
await this.simulateDelay(1000);
|
||||
const newReturn: ReturnApplication = {
|
||||
id: `RET-${Date.now()}`,
|
||||
orderId: data.orderId,
|
||||
returnReason: data.returnReason,
|
||||
returnDescription: data.returnDescription,
|
||||
returnItems: data.returnItems,
|
||||
images: data.images,
|
||||
contactPhone: data.contactPhone,
|
||||
status: 'PENDING',
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
this.mockReturns.push(newReturn);
|
||||
return newReturn;
|
||||
}
|
||||
|
||||
async fetchCustomerServiceTickets(params?: AfterSalesQueryParams): Promise<CustomerServiceTicket[]> {
|
||||
await this.simulateDelay(300);
|
||||
return [
|
||||
{
|
||||
id: 'TICKET-001',
|
||||
orderId: 'order-001',
|
||||
customerId: 'C001',
|
||||
customerName: 'John Doe',
|
||||
customerEmail: 'john@example.com',
|
||||
subject: 'Product not received',
|
||||
category: 'delivery',
|
||||
priority: 'HIGH',
|
||||
status: 'OPEN',
|
||||
createdAt: '2026-03-18T10:00:00Z',
|
||||
updatedAt: '2026-03-18T10:00:00Z',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
async fetchRefundRecords(params?: AfterSalesQueryParams): Promise<RefundRecord[]> {
|
||||
await this.simulateDelay(300);
|
||||
return [
|
||||
{
|
||||
id: 'REFUND-001',
|
||||
orderId: 'order-001',
|
||||
refundId: 'RF-001',
|
||||
customerId: 'C001',
|
||||
customerName: 'John Doe',
|
||||
amount: 59.98,
|
||||
currency: 'USD',
|
||||
reason: 'Defective product',
|
||||
status: 'PENDING',
|
||||
createdAt: '2026-03-18T10:00:00Z',
|
||||
updatedAt: '2026-03-18T10:00:00Z',
|
||||
platform: 'Amazon',
|
||||
refundMethod: 'Original Payment',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
private simulateDelay(ms: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
@@ -36,10 +36,12 @@ export interface BatchOrder {
|
||||
totalItems: number;
|
||||
totalAmount: number;
|
||||
currency: string;
|
||||
status: 'DRAFT' | 'PENDING_REVIEW' | 'CONFIRMED' | 'PROCESSING' | 'COMPLETED' | 'CANCELLED';
|
||||
status: 'DRAFT' | 'PENDING_REVIEW' | 'CONFIRMED' | 'PROCESSING' | 'COMPLETED' | 'CANCELLED' | 'SUBMITTED';
|
||||
createdAt: string;
|
||||
validItems: number;
|
||||
invalidItems: number;
|
||||
items?: BatchOrderItem[];
|
||||
estimatedDelivery?: string;
|
||||
}
|
||||
|
||||
export interface Customer {
|
||||
@@ -47,6 +49,7 @@ export interface Customer {
|
||||
name: string;
|
||||
company: string;
|
||||
tier: 'BASIC' | 'PRO' | 'ENTERPRISE';
|
||||
email?: string;
|
||||
}
|
||||
|
||||
export interface BatchOrderQueryParams {
|
||||
@@ -64,6 +67,37 @@ export interface BatchOrderStats {
|
||||
totalAmount: number;
|
||||
}
|
||||
|
||||
export interface Contract {
|
||||
id: string;
|
||||
contractId: string;
|
||||
contractNumber?: string;
|
||||
customerId: string;
|
||||
customerName: string;
|
||||
type: 'PURCHASE' | 'SALES' | 'SERVICE' | 'DISTRIBUTION' | 'OEM' | 'NDA';
|
||||
title: string;
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
totalAmount: number;
|
||||
totalValue?: number;
|
||||
currency: string;
|
||||
status: 'DRAFT' | 'PENDING_REVIEW' | 'ACTIVE' | 'EXPIRED' | 'TERMINATED' | 'RENEWED';
|
||||
signedDate?: string;
|
||||
signedBy?: string;
|
||||
attachments?: string[];
|
||||
terms?: string;
|
||||
remark?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface ContractQueryParams {
|
||||
customerId?: string;
|
||||
status?: string;
|
||||
type?: string;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// B2B Trade专用接口
|
||||
// ============================================
|
||||
@@ -77,6 +111,10 @@ export interface IB2BTradeDataSource {
|
||||
approveBatchOrder(batchId: string): Promise<BatchOrder>;
|
||||
validateItems(items: BatchOrderItem[]): Promise<BatchOrderItem[]>;
|
||||
parseUploadFile(file: File, customerId: string): Promise<BatchOrderItem[]>;
|
||||
fetchContracts(params?: ContractQueryParams): Promise<Contract[]>;
|
||||
fetchContractDetail(contractId: string): Promise<Contract | null>;
|
||||
createContract(data: Partial<Contract>): Promise<Contract>;
|
||||
updateContract(contractId: string, data: Partial<Contract>): Promise<Contract>;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
@@ -148,6 +186,39 @@ class ApiB2BTradeDataSource implements IB2BTradeDataSource {
|
||||
const result = await response.json();
|
||||
return result.data;
|
||||
}
|
||||
|
||||
async fetchContracts(params?: ContractQueryParams): Promise<Contract[]> {
|
||||
const query = new URLSearchParams(params as Record<string, string>).toString();
|
||||
const response = await fetch(`/api/v1/b2b/contracts?${query}`);
|
||||
const result = await response.json();
|
||||
return result.data;
|
||||
}
|
||||
|
||||
async fetchContractDetail(contractId: string): Promise<Contract | null> {
|
||||
const response = await fetch(`/api/v1/b2b/contracts/${contractId}`);
|
||||
const result = await response.json();
|
||||
return result.data;
|
||||
}
|
||||
|
||||
async createContract(data: Partial<Contract>): Promise<Contract> {
|
||||
const response = await fetch('/api/v1/b2b/contracts', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
const result = await response.json();
|
||||
return result.data;
|
||||
}
|
||||
|
||||
async updateContract(contractId: string, data: Partial<Contract>): Promise<Contract> {
|
||||
const response = await fetch(`/api/v1/b2b/contracts/${contractId}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
const result = await response.json();
|
||||
return result.data;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
@@ -291,6 +362,60 @@ class MockB2BTradeDataSource implements IB2BTradeDataSource {
|
||||
];
|
||||
}
|
||||
|
||||
async fetchContracts(params?: ContractQueryParams): Promise<Contract[]> {
|
||||
await this.simulateDelay(400);
|
||||
return [
|
||||
{
|
||||
id: '1',
|
||||
contractId: 'CON-2026-001',
|
||||
customerId: 'CUST_001',
|
||||
customerName: 'ABC Trading Co.',
|
||||
type: 'PURCHASE',
|
||||
title: 'Annual Purchase Agreement',
|
||||
startDate: '2026-01-01',
|
||||
endDate: '2026-12-31',
|
||||
totalAmount: 500000,
|
||||
currency: 'USD',
|
||||
status: 'ACTIVE',
|
||||
createdAt: '2026-01-01',
|
||||
updatedAt: '2026-01-01',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
async fetchContractDetail(contractId: string): Promise<Contract | null> {
|
||||
await this.simulateDelay(300);
|
||||
const contracts = await this.fetchContracts();
|
||||
return contracts.find(c => c.contractId === contractId) || null;
|
||||
}
|
||||
|
||||
async createContract(data: Partial<Contract>): Promise<Contract> {
|
||||
await this.simulateDelay(500);
|
||||
const newContract: Contract = {
|
||||
id: `${Date.now()}`,
|
||||
contractId: `CON-${Date.now()}`,
|
||||
customerId: data.customerId || '',
|
||||
customerName: data.customerName || '',
|
||||
type: data.type || 'PURCHASE',
|
||||
title: data.title || '',
|
||||
startDate: data.startDate || '',
|
||||
endDate: data.endDate || '',
|
||||
totalAmount: data.totalAmount || 0,
|
||||
currency: data.currency || 'USD',
|
||||
status: 'DRAFT',
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
return newContract;
|
||||
}
|
||||
|
||||
async updateContract(contractId: string, data: Partial<Contract>): Promise<Contract> {
|
||||
await this.simulateDelay(300);
|
||||
const contract = await this.fetchContractDetail(contractId);
|
||||
if (!contract) throw new Error('Contract not found');
|
||||
return { ...contract, ...data, updatedAt: new Date().toISOString() };
|
||||
}
|
||||
|
||||
private simulateDelay(ms: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
@@ -41,8 +41,13 @@ export interface RiskAssessment {
|
||||
orderId: string;
|
||||
customerId: string;
|
||||
customerName: string;
|
||||
platform?: string;
|
||||
platform_buyer_id?: string;
|
||||
riskScore: number;
|
||||
risk_score?: number;
|
||||
riskLevel: 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL';
|
||||
risk_level?: 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL';
|
||||
assessment_date?: string;
|
||||
riskFactors: string[];
|
||||
status: 'PENDING' | 'REVIEWING' | 'APPROVED' | 'REJECTED';
|
||||
reviewedBy?: string;
|
||||
|
||||
Reference in New Issue
Block a user