feat: 添加部门管理功能、主题切换和多语言支持

refactor(dashboard): 重构用户管理页面和路由结构

feat(server): 实现部门管理API和RBAC增强功能

docs: 更新用户手册和管理员指南文档

style: 统一图标使用和组件命名规范

test: 添加部门服务和数据隔离测试用例

chore: 更新依赖和配置文件
This commit is contained in:
2026-03-28 22:52:12 +08:00
parent 22308fe042
commit d327706087
87 changed files with 21372 additions and 4806 deletions

View File

@@ -214,11 +214,11 @@ const ProductPublishForm: FC = () => {
// 模拟保存到本地数据库
await new Promise(resolve => setTimeout(resolve, 800));
message.success('Product saved to local system');
message.success(t('product.publish.productSaved', { name: values.name }));
// Step 2: 合规检查(本地进行)
if (values.complianceCheck) {
message.info('Running compliance check...');
message.info(t('product.publish.runComplianceCheck'));
await new Promise(resolve => setTimeout(resolve, 600));
}
@@ -226,14 +226,14 @@ const ProductPublishForm: FC = () => {
await new Promise(resolve => setTimeout(resolve, 400));
Modal.success({
title: 'Product Created Successfully',
title: t('product.publish.productCreated'),
content: (
<div>
<p>Product "{values.name}" has been saved to local system.</p>
<p>Target platforms: {values.platform.join(', ')}</p>
<p>Status: {values.autoPublish ? 'Will be auto-published after review' : 'Pending review'}</p>
<p>{t('product.publish.productSaved', { name: values.name })}</p>
<p>{t('product.publish.targetPlatformsList', { platforms: values.platform.join(', ') })}</p>
<p>Status: {values.autoPublish ? t('product.publish.statusAuto') : t('product.publish.statusPending')}</p>
<p style={{ color: '#1890ff', marginTop: 8 }}>
The product will be synced to platforms according to the sync schedule.
{t('product.publish.syncSchedule')}
</p>
</div>
),
@@ -244,7 +244,7 @@ const ProductPublishForm: FC = () => {
setVariants([]);
setFileList([]);
} catch (error) {
message.error('Please fill in required fields');
message.error(t('product.publish.fillRequiredFields'));
} finally {
setLoading(false);
}
@@ -252,31 +252,31 @@ const ProductPublishForm: FC = () => {
const variantColumns = [
{
title: 'SKU',
title: t('product.publish.sku'),
dataIndex: 'sku',
key: 'sku',
render: (value: string, record: ProductVariant) => (
<Input
value={value}
onChange={(e) => handleVariantChange(record.id, 'sku', e.target.value)}
placeholder="SKU"
placeholder={t('product.publish.sku')}
/>
),
},
{
title: 'Name',
title: t('product.publish.variantName'),
dataIndex: 'name',
key: 'name',
render: (value: string, record: ProductVariant) => (
<Input
value={value}
onChange={(e) => handleVariantChange(record.id, 'name', e.target.value)}
placeholder="Variant Name"
placeholder={t('product.publish.variantName')}
/>
),
},
{
title: 'Price',
title: t('product.publish.price'),
dataIndex: 'price',
key: 'price',
render: (value: number, record: ProductVariant) => (
@@ -290,7 +290,7 @@ const ProductPublishForm: FC = () => {
),
},
{
title: 'Stock',
title: t('product.publish.stock'),
dataIndex: 'stock',
key: 'stock',
render: (value: number, record: ProductVariant) => (
@@ -303,7 +303,7 @@ const ProductPublishForm: FC = () => {
),
},
{
title: 'Action',
title: t('product.publish.action'),
key: 'action',
render: (_: any, record: ProductVariant) => (
<Button
@@ -311,6 +311,7 @@ const ProductPublishForm: FC = () => {
danger
icon={<DeleteOutlined />}
onClick={() => handleRemoveVariant(record.id)}
title={t('common.delete')}
/>
),
},
@@ -318,7 +319,7 @@ const ProductPublishForm: FC = () => {
return (
<div className="product-publish-form-page">
<Card title="Product Publish Form">
<Card title={t('product.publish.formTitle')}>
<Form
form={form}
layout="vertical"
@@ -333,25 +334,25 @@ const ProductPublishForm: FC = () => {
activeKey={activeTab}
onChange={setActiveTab}
items={[
{ key: 'basic', label: 'Basic Info', children: (
{ key: 'basic', label: t('product.publish.basicInfoTab'), children: (
<>
<Row gutter={24}>
<Col span={12}>
<Form.Item
name="name"
label="Product Name"
rules={[{ required: true, message: 'Please enter product name' }]}
label={t('product.publish.productName')}
rules={[{ required: true, message: t('product.publish.enterProductName') }]}
>
<Input placeholder="Enter product name" maxLength={200} showCount />
<Input placeholder={t('product.publish.enterProductName')} maxLength={200} showCount />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
name="platform"
label="Target Platforms"
rules={[{ required: true, message: 'Please select at least one platform' }]}
label={t('product.publish.targetPlatforms')}
rules={[{ required: true, message: t('product.publish.selectPlatform') }]}
>
<Select mode="multiple" placeholder="Select platforms">
<Select mode="multiple" placeholder={t('product.publish.selectPlatform')}>
{PLATFORMS.map(p => (
<Option key={p.value} value={p.value}>{p.label}</Option>
))}
@@ -364,10 +365,10 @@ const ProductPublishForm: FC = () => {
<Col span={12}>
<Form.Item
name="category"
label="Category"
rules={[{ required: true, message: 'Please select category' }]}
label={t('product.publish.category')}
rules={[{ required: true, message: t('product.publish.selectCategory') }]}
>
<Select placeholder="Select category" showSearch>
<Select placeholder={t('product.publish.selectCategory')} showSearch>
{CATEGORIES.map(c => (
<Option key={c.value} value={c.value}>{c.label}</Option>
))}
@@ -375,18 +376,18 @@ const ProductPublishForm: FC = () => {
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name="brand" label="Brand">
<Input placeholder="Enter brand name" />
<Form.Item name="brand" label={t('product.publish.brand')}>
<Input placeholder={t('product.publish.brand')} />
</Form.Item>
</Col>
</Row>
<Form.Item
name="description"
label="Description"
rules={[{ required: true, message: 'Please enter description' }]}
label={t('product.publish.description')}
rules={[{ required: true, message: t('product.publish.enterDescription') }]}
>
<TextArea rows={4} placeholder="Enter product description" maxLength={5000} showCount />
<TextArea rows={4} placeholder={t('product.publish.enterDescription')} maxLength={5000} showCount />
</Form.Item>
<Divider>{t('product.publish.productImages')}</Divider>
@@ -521,34 +522,34 @@ const ProductPublishForm: FC = () => {
</Row>
</>
)},
{ key: 'inventory', label: <><BoxPlotOutlined /> Inventory</>, children: (
{ key: 'inventory', label: <><BoxPlotOutlined /> {t('product.publish.inventoryTab')}</>, children: (
<>
<Row gutter={24}>
<Col span={12}>
<Form.Item
name="sku"
label="SKU"
rules={[{ required: true, message: 'Please enter SKU' }]}
label={t('product.publish.sku')}
rules={[{ required: true, message: t('product.publish.enterSku') }]}
>
<Input placeholder="Enter SKU" />
<Input placeholder={t('product.publish.enterSku')} />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
name="stock"
label="Initial Stock"
rules={[{ required: true, message: 'Please enter stock quantity' }]}
label={t('product.publish.stock')}
rules={[{ required: true, message: t('product.publish.enterStock') }]}
>
<InputNumber min={0} style={{ width: '100%' }} placeholder="0" />
</Form.Item>
</Col>
</Row>
<Divider>Variants</Divider>
<Divider>{t('product.publish.variantName')}</Divider>
<div style={{ marginBottom: 16 }}>
<Button type="dashed" icon={<PlusOutlined />} onClick={handleAddVariant}>
Add Variant
{t('product.publish.addVariant')}
</Button>
</div>
@@ -558,23 +559,23 @@ const ProductPublishForm: FC = () => {
rowKey="id"
pagination={false}
size="small"
locale={{ emptyText: 'No variants added' }}
locale={{ emptyText: t('product.publish.noVariants') }}
/>
</>
)},
{ key: 'settings', label: 'Publish Settings', children: (
{ key: 'settings', label: t('product.publish.settingsTab'), children: (
<>
<Row gutter={24}>
<Col span={12}>
<Form.Item
name="status"
label="Status"
label={t('product.publish.status')}
rules={[{ required: true }]}
>
<Select>
<Option value="DRAFT">Draft</Option>
<Option value="PENDING">Pending Review</Option>
<Option value="ACTIVE">Active</Option>
<Option value="DRAFT">{t('common.draft')}</Option>
<Option value="PENDING">{t('product.publish.statusPending')}</Option>
<Option value="ACTIVE">{t('common.active')}</Option>
</Select>
</Form.Item>
</Col>
@@ -582,29 +583,29 @@ const ProductPublishForm: FC = () => {
<Form.Item
name="complianceCheck"
label="Run Compliance Check"
label={t('product.publish.runComplianceCheck')}
valuePropName="checked"
>
<Switch checkedChildren="Yes" unCheckedChildren="No" />
<Switch checkedChildren={t('common.yes')} unCheckedChildren={t('common.no')} />
</Form.Item>
<Form.Item
name="autoPublish"
label="Auto Publish After Compliance Check"
label={t('product.publish.autoPublish')}
valuePropName="checked"
extra="Product will be automatically published if compliance check passes"
extra={t('product.publish.autoPublishDesc')}
>
<Switch checkedChildren="Yes" unCheckedChildren="No" />
<Switch checkedChildren={t('common.yes')} unCheckedChildren={t('common.no')} />
</Form.Item>
<Alert
message="Publishing Rules"
message={t('product.publish.publishingRules')}
description={
<ul style={{ marginBottom: 0, paddingLeft: 20 }}>
<li>Products are first saved to local system, then synced to platforms</li>
<li>All products must pass compliance check before syncing</li>
<li>Products with profit margin below threshold will require manual approval</li>
<li>Platform sync happens according to configured schedule</li>
<li>{t('product.publish.rule1')}</li>
<li>{t('product.publish.rule2')}</li>
<li>{t('product.publish.rule3')}</li>
<li>{t('product.publish.rule4')}</li>
</ul>
}
type="warning"
@@ -618,12 +619,12 @@ const ProductPublishForm: FC = () => {
<Divider />
<Space style={{ width: '100%', justifyContent: 'flex-end' }}>
<Button onClick={() => form.resetFields()}>Reset</Button>
<Button onClick={() => form.resetFields()}>{t('product.publish.reset')}</Button>
<Button icon={<SaveOutlined />} onClick={handleSaveDraft} loading={loading}>
Save Draft
{t('product.publish.saveDraft')}
</Button>
<Button type="primary" icon={<SendOutlined />} onClick={handleSubmit} loading={loading}>
Save to System
{t('product.publish.saveToSystem')}
</Button>
</Space>
</Form>

View File

@@ -32,21 +32,10 @@ import {
FC,
} from '@/imports';
import CrossPlatformManage from './CrossPlatformManage';
interface Product {
id: string;
name: string;
sku: string;
price: number;
cost: number;
stock: number;
status: 'draft' | 'published' | 'synced';
platform: string;
createdAt: string;
}
import { productDataSource, Product as ProductType } from '@/services/productDataSource';
const ProductManagement: FC = () => {
const [products, setProducts] = useState<Product[]>([]);
const [products, setProducts] = useState<ProductType[]>([]);
const [loading, setLoading] = useState(false);
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
const [searchText, setSearchText] = useState('');
@@ -54,11 +43,11 @@ const ProductManagement: FC = () => {
const [sortField, setSortField] = useState<string>('');
const [sortOrder, setSortOrder] = useState<'ascend' | 'descend'>('ascend');
const [isModalVisible, setIsModalVisible] = useState(false);
const [editingProduct, setEditingProduct] = useState<Product | null>(null);
const [editingProduct, setEditingProduct] = useState<ProductType | null>(null);
const [isDetailModalVisible, setIsDetailModalVisible] = useState(false);
const [viewingProduct, setViewingProduct] = useState<Product | null>(null);
const [viewingProduct, setViewingProduct] = useState<ProductType | null>(null);
const [isPricingModalVisible, setIsPricingModalVisible] = useState(false);
const [pricingProduct, setPricingProduct] = useState<Product | null>(null);
const [pricingProduct, setPricingProduct] = useState<ProductType | null>(null);
const [activeTab, setActiveTab] = useState('local');
const [form] = Form.useForm();
const [pricingForm] = Form.useForm();
@@ -70,9 +59,8 @@ const ProductManagement: FC = () => {
const fetchProducts = async () => {
setLoading(true);
try {
const response = await fetch('/api/v1/products');
const data = await response.json();
setProducts(data.data || []);
const data = await productDataSource.fetchProducts();
setProducts(data || []);
} catch (error) {
message.error('获取商品列表失败');
} finally {
@@ -99,33 +87,31 @@ const ProductManagement: FC = () => {
setIsModalVisible(true);
};
const handleEdit = (record: Product) => {
const handleEdit = (record: ProductType) => {
setEditingProduct(record);
form.setFieldsValue(record);
setIsModalVisible(true);
};
const handleView = (record: Product) => {
const handleView = (record: ProductType) => {
setViewingProduct(record);
setIsDetailModalVisible(true);
};
const handlePricing = (record: Product) => {
const handlePricing = (record: ProductType) => {
setPricingProduct(record);
pricingForm.setFieldsValue({
cost: record.cost,
cost: record.costPrice,
price: record.price,
profit: record.price - record.cost,
profitMargin: ((record.price - record.cost) / record.price * 100).toFixed(2)
profit: record.profit,
profitMargin: ((record.profit / record.price) * 100).toFixed(2)
});
setIsPricingModalVisible(true);
};
const handlePublish = async (record: Product) => {
const handlePublish = async (record: ProductType) => {
try {
await fetch(`/api/v1/products/${record.id}/publish`, {
method: 'POST'
});
await productDataSource.updateProductStatus(record.id, 'LISTED');
message.success('商品上架成功');
fetchProducts();
} catch (error) {
@@ -133,11 +119,9 @@ const ProductManagement: FC = () => {
}
};
const handleSync = async (record: Product) => {
const handleSync = async (record: ProductType) => {
try {
await fetch(`/api/v1/products/${record.id}/sync`, {
method: 'POST'
});
await productDataSource.syncProduct(record.id);
message.success('商品同步成功');
fetchProducts();
} catch (error) {
@@ -151,11 +135,10 @@ const ProductManagement: FC = () => {
return;
}
try {
await fetch('/api/v1/products/batch-sync', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ids: selectedRowKeys })
});
// 逐个同步商品
for (const id of selectedRowKeys) {
await productDataSource.syncProduct(id as string);
}
message.success('批量同步成功');
fetchProducts();
} catch (error) {
@@ -165,9 +148,7 @@ const ProductManagement: FC = () => {
const handleDelete = async (id: string) => {
try {
await fetch(`/api/v1/products/${id}`, {
method: 'DELETE'
});
await productDataSource.deleteProduct(id);
message.success('商品删除成功');
fetchProducts();
} catch (error) {
@@ -179,17 +160,24 @@ const ProductManagement: FC = () => {
try {
const values = await form.validateFields();
if (editingProduct) {
await fetch(`/api/v1/products/${editingProduct.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(values)
});
await productDataSource.updateProduct(editingProduct.id, values);
message.success('商品更新成功');
} else {
await fetch('/api/v1/products', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(values)
await productDataSource.createProduct({
sku: values.sku,
name: values.name,
image: values.image || 'https://via.placeholder.com/80x80?text=Product',
category: values.category || '工业自动化',
price: values.price,
costPrice: values.costPrice || 0,
profit: values.profit || 0,
roi: values.roi || 0,
stock: values.stock || 0,
status: 'DRAFT',
platformStatus: {},
shopId: 'shop-tiktok-1',
shopName: 'TikTok旗舰店',
platform: 'TikTok',
});
message.success('商品创建成功');
}
@@ -215,10 +203,10 @@ const ProductManagement: FC = () => {
try {
const values = await pricingForm.validateFields();
if (pricingProduct) {
await fetch(`/api/v1/products/${pricingProduct.id}/pricing`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(values)
await productDataSource.updateProduct(pricingProduct.id, {
price: values.price,
costPrice: values.cost,
profit: values.profit
});
message.success('定价更新成功');
setIsPricingModalVisible(false);
@@ -241,7 +229,7 @@ const ProductManagement: FC = () => {
});
const sortedProducts = [...filteredProducts].sort((a, b) => {
const field = sortField as keyof Product;
const field = sortField as keyof ProductType;
const aValue = a[field];
const bValue = b[field];
@@ -273,10 +261,10 @@ const ProductManagement: FC = () => {
},
{
title: '成本',
dataIndex: 'cost',
key: 'cost',
dataIndex: 'costPrice',
key: 'costPrice',
sorter: true,
render: (cost: number) => `¥${cost.toFixed(2)}`
render: (costPrice: number) => `¥${costPrice.toFixed(2)}`
},
{
title: '库存',
@@ -289,32 +277,32 @@ const ProductManagement: FC = () => {
dataIndex: 'status',
key: 'status',
filters: [
{ text: '草稿', value: 'draft' },
{ text: '已定价', value: 'priced' },
{ text: '已上架', value: 'listed' },
{ text: '同步中', value: 'syncing' },
{ text: '已在线', value: 'live' },
{ text: '同步失败', value: 'sync_failed' },
{ text: '已下架', value: 'offline' }
{ text: '草稿', value: 'DRAFT' },
{ text: '已定价', value: 'PRICED' },
{ text: '已上架', value: 'LISTED' },
{ text: '同步中', value: 'SYNCING' },
{ text: '已在线', value: 'LIVE' },
{ text: '同步失败', value: 'SYNC_FAILED' },
{ text: '已下架', value: 'OFFLINE' }
],
render: (status: string) => {
const colorMap: Record<string, string> = {
draft: 'default',
priced: 'processing',
listed: 'warning',
syncing: 'processing',
live: 'success',
sync_failed: 'error',
offline: 'default'
DRAFT: 'default',
PRICED: 'processing',
LISTED: 'warning',
SYNCING: 'processing',
LIVE: 'success',
SYNC_FAILED: 'error',
OFFLINE: 'default'
};
const textMap: Record<string, string> = {
draft: '草稿',
priced: '已定价',
listed: '已上架(本地)',
syncing: '同步中',
live: '已在线',
sync_failed: '同步失败',
offline: '已下架'
DRAFT: '草稿',
PRICED: '已定价',
LISTED: '已上架(本地)',
SYNCING: '同步中',
LIVE: '已在线',
SYNC_FAILED: '同步失败',
OFFLINE: '已下架'
};
return <Tag color={colorMap[status]}>{textMap[status]}</Tag>;
}
@@ -327,7 +315,7 @@ const ProductManagement: FC = () => {
{
title: '操作',
key: 'action',
render: (_: any, record: Product) => (
render: (_: any, record: ProductType) => (
<Space size="small">
<Button
type="link"
@@ -349,7 +337,7 @@ const ProductManagement: FC = () => {
>
</Button>
{record.status === 'draft' && (
{record.status === 'DRAFT' && (
<Button
type="link"
onClick={() => handlePublish(record)}
@@ -417,9 +405,13 @@ const ProductManagement: FC = () => {
onChange={handleFilter}
allowClear
>
<Select.Option value="draft">稿</Select.Option>
<Select.Option value="published"></Select.Option>
<Select.Option value="synced"></Select.Option>
<Select.Option value="DRAFT">稿</Select.Option>
<Select.Option value="PRICED"></Select.Option>
<Select.Option value="LISTED"></Select.Option>
<Select.Option value="SYNCING"></Select.Option>
<Select.Option value="LIVE">线</Select.Option>
<Select.Option value="SYNC_FAILED"></Select.Option>
<Select.Option value="OFFLINE"></Select.Option>
</Select>
<Select
placeholder="排序字段"
@@ -510,7 +502,7 @@ const ProductManagement: FC = () => {
/>
</Form.Item>
<Form.Item
name="cost"
name="costPrice"
label="成本"
rules={[{ required: true, message: '请输入成本' }]}
>
@@ -538,9 +530,16 @@ const ProductManagement: FC = () => {
rules={[{ required: true, message: '请选择平台' }]}
>
<Select placeholder="请选择平台">
<Select.Option value="amazon">Amazon</Select.Option>
<Select.Option value="ebay">eBay</Select.Option>
<Select.Option value="shopify">Shopify</Select.Option>
<Select.Option value="TikTok">TikTok</Select.Option>
<Select.Option value="Shopee">Shopee</Select.Option>
<Select.Option value="Lazada">Lazada</Select.Option>
<Select.Option value="Amazon">Amazon</Select.Option>
<Select.Option value="eBay">eBay</Select.Option>
<Select.Option value="Shopify">Shopify</Select.Option>
<Select.Option value="TemuFull">Temu</Select.Option>
<Select.Option value="Shein">Shein</Select.Option>
<Select.Option value="AliExpress">AliExpress</Select.Option>
<Select.Option value="Walmart">Walmart</Select.Option>
</Select>
</Form.Item>
</Form>
@@ -576,11 +575,11 @@ const ProductManagement: FC = () => {
</Col>
<Col span={8}>
<Text strong></Text>
<div>¥{viewingProduct.cost.toFixed(2)}</div>
<div>¥{viewingProduct.costPrice.toFixed(2)}</div>
</Col>
<Col span={8}>
<Text strong></Text>
<div>¥{(viewingProduct.price - viewingProduct.cost).toFixed(2)}</div>
<div>¥{viewingProduct.profit.toFixed(2)}</div>
</Col>
</Row>
<Row gutter={16} style={{ marginTop: 16 }}>
@@ -591,9 +590,27 @@ const ProductManagement: FC = () => {
<Col span={8}>
<Text strong></Text>
<div>
<Tag color={viewingProduct.status === 'published' ? 'green' : 'default'}>
{viewingProduct.status === 'published' ? '已发布' : viewingProduct.status === 'synced' ? '已同步' : '草稿'}
</Tag>
{(() => {
const colorMap: Record<string, string> = {
DRAFT: 'default',
PRICED: 'processing',
LISTED: 'warning',
SYNCING: 'processing',
LIVE: 'success',
SYNC_FAILED: 'error',
OFFLINE: 'default'
};
const textMap: Record<string, string> = {
DRAFT: '草稿',
PRICED: '已定价',
LISTED: '已上架(本地)',
SYNCING: '同步中',
LIVE: '已在线',
SYNC_FAILED: '同步失败',
OFFLINE: '已下架'
};
return <Tag color={colorMap[viewingProduct.status]}>{textMap[viewingProduct.status]}</Tag>;
})()}
</div>
</Col>
<Col span={8}>