feat: 开发采集、采集箱和商品编辑功能

This commit is contained in:
Joey
2026-08-15 22:17:26 +08:00
parent c61d1a3154
commit 36357843d0
130 changed files with 18005 additions and 12 deletions
@@ -0,0 +1,161 @@
import { useState } from 'react';
import { Col, Divider, Input, Row, Tag, Typography } from 'antd';
import { ProductDetail } from '@/services/product';
import CopyPanel from './CopyPanel';
const { Text } = Typography;
interface Props {
product: ProductDetail;
onSave: (p: Partial<ProductDetail>) => Promise<void> | void;
}
/** 产品属性:采集参数只读展示 / 条形码 / 品牌 / 简介 / AI 文案 */
export default function ProductAttributesPanel({ product, onSave }: Props) {
const raw = (product.raw ?? {}) as Record<string, unknown>;
const params = raw.params as Array<{ key: string; value: string }> | undefined;
const [descExpanded, setDescExpanded] = useState(false);
const updateRaw = (patch: Record<string, unknown>) => {
onSave({ raw: { ...raw, ...patch } });
};
const sellingPoints = typeof raw.sellingPoints === 'string' ? raw.sellingPoints : '';
const desc = typeof raw.desc === 'string' ? raw.desc : '';
return (
<div>
{/* 基础字段:品牌 + 条形码 */}
<Row gutter={16} style={{ marginBottom: 12 }}>
<Col span={8}>
<Text style={{ fontSize: 12, color: '#666', display: 'block', marginBottom: 4 }}>
Brand
</Text>
<Input
value={(raw.brand as string) ?? ''}
onChange={(e) => updateRaw({ brand: e.target.value })}
placeholder="采集到的品牌名"
/>
</Col>
<Col span={8}>
<Text style={{ fontSize: 12, color: '#666', display: 'block', marginBottom: 4 }}>
Barcode
</Text>
<Input
value={product.barcode ?? ''}
onChange={(e) => onSave({ barcode: e.target.value })}
placeholder="EAN / UPC / 留空"
/>
</Col>
<Col span={8}>
<Text style={{ fontSize: 12, color: '#666', display: 'block', marginBottom: 4 }}>
</Text>
<Input
value={(raw.tags as string) ?? ''}
onChange={(e) => updateRaw({ tags: e.target.value })}
placeholder="逗号分隔,内部用"
/>
</Col>
</Row>
{/* 俄文简介(来自 AI 生成或手动) */}
<div style={{ marginBottom: 12 }}>
<Text style={{ fontSize: 12, color: '#666', display: 'block', marginBottom: 4 }}>
description
</Text>
<Input.TextArea
autoSize={{ minRows: 3, maxRows: 8 }}
value={product.description ?? ''}
onChange={(e) => onSave({ description: e.target.value })}
placeholder="Описание товара на русском — можно сгенерировать через AI ниже"
/>
</div>
{/* 采集参数展示(只读,参考用) */}
{Array.isArray(params) && params.length > 0 && (
<div style={{ marginBottom: 12, padding: '12px 16px', background: '#fafafa', borderRadius: 8 }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 8 }}>
<Text style={{ fontSize: 12, color: '#666', fontWeight: 500 }}>
{params.length}
</Text>
{params.length > 8 && (
<a style={{ fontSize: 12 }} onClick={() => setDescExpanded(!descExpanded)}>
{descExpanded ? '收起' : `展开全部 ${params.length}`}
</a>
)}
</div>
<Row gutter={[8, 4]}>
{(descExpanded ? params : params.slice(0, 8)).map((p, i) => (
<Col key={i} span={12}>
<div style={{ display: 'flex', gap: 6, alignItems: 'baseline' }}>
<Tag
style={{
fontSize: 11,
maxWidth: 120,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
flexShrink: 0,
}}
>
{p.key}
</Tag>
<Text style={{ fontSize: 12 }} ellipsis={{ tooltip: p.value }}>
{p.value}
</Text>
</div>
</Col>
))}
</Row>
{!descExpanded && params.length > 8 && (
<Text type="secondary" style={{ fontSize: 11, marginTop: 4, display: 'block' }}>
{params.length - 8}
</Text>
)}
</div>
)}
{/* 采集卖点 / 描述(只读参考) */}
{(sellingPoints || desc) && (
<div style={{ marginBottom: 12, padding: '12px 16px', background: '#fafafa', borderRadius: 8 }}>
{sellingPoints && (
<>
<Text style={{ fontSize: 12, color: '#666', fontWeight: 500, display: 'block', marginBottom: 4 }}>
</Text>
<Text style={{ fontSize: 12, whiteSpace: 'pre-wrap' }}>
{sellingPoints}
</Text>
</>
)}
{desc && (
<>
<Text
style={{
fontSize: 12,
color: '#666',
fontWeight: 500,
display: 'block',
marginTop: sellingPoints ? 8 : 0,
marginBottom: 4,
}}
>
</Text>
<Text style={{ fontSize: 12, whiteSpace: 'pre-wrap' }}>
{desc.slice(0, 300)}
{desc.length > 300 && '…'}
</Text>
</>
)}
</div>
)}
<Divider style={{ margin: '16px 0' }} />
<CopyPanel product={product} onSave={onSave} />
</div>
);
}