Files
ozon-seller-kit/studio/src/pages/trial/TrialPage.tsx
T

164 lines
6.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 商品试算页(docs/v2.1/trial-page.md):采集后的主工作流。
* 01 商品信息 → 02 价格试算 → 03 俄文文案 → 04 图片与AI生图 → 05 入库与导出。
* 操作流水线对齐 v1 web 工具台;数据自动落库(products 表)。
*/
import { useCallback, useEffect, useState } from 'react';
import { Link, useParams } from 'react-router';
import { Card, Menu, Space, Spin, Typography, message } from 'antd';
import { getProduct, listAssets, ProductDetail, ProductAsset, updateProduct } from '@/services/product';
import { apiErrorMessage } from '@/services/api';
import { STAGE_COLOR, STAGE_LABEL } from '../collection/CollectionPage';
import TrialInfoPanel from './TrialInfoPanel';
import TrialPricingPanel from './TrialPricingPanel';
import TrialSuitePanel from './TrialSuitePanel';
import TrialExportPanel from './TrialExportPanel';
import CopyPanel from '../product/CopyPanel';
const { Title, Text } = Typography;
const SECTIONS = [
{ id: 'section-info', label: '商品信息' },
{ id: 'section-pricing', label: '价格试算' },
{ id: 'section-copy', label: '俄文文案' },
{ id: 'section-collect', label: '采集图片' },
{ id: 'section-plan', label: '出图方案' },
{ id: 'section-result', label: '生成结果' },
{ id: 'section-export', label: '入库与导出' },
];
export default function TrialPage() {
const { id } = useParams();
const [product, setProduct] = useState<ProductDetail | null>(null);
const [assets, setAssets] = useState<ProductAsset[]>([]);
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [active, setActive] = useState(SECTIONS[0].id);
const load = useCallback(async () => {
if (!id) return;
setLoading(true);
try {
const [p, a] = await Promise.all([getProduct(id), listAssets(id)]);
setProduct(p);
setAssets(a);
} catch (e) {
message.error(apiErrorMessage(e));
} finally {
setLoading(false);
}
}, [id]);
useEffect(() => {
load();
}, [load]);
// 滚动监听:高亮当前区域
useEffect(() => {
const onScroll = () => {
let current = SECTIONS[0].id;
for (const s of SECTIONS) {
const el = document.getElementById(s.id);
if (el && el.getBoundingClientRect().top <= 90) current = s.id;
}
setActive(current);
};
window.addEventListener('scroll', onScroll, { passive: true });
return () => window.removeEventListener('scroll', onScroll);
}, []);
const save = useCallback(
async (partial: Partial<ProductDetail>) => {
if (!id || !product) return;
setSaving(true);
try {
const updated = await updateProduct(id, partial);
setProduct(updated);
} catch (e) {
message.error(apiErrorMessage(e));
} finally {
setSaving(false);
}
},
[id, product],
);
if (loading || !product) {
return (
<div style={{ padding: 80, textAlign: 'center' }}>
<Spin size="large" />
</div>
);
}
const raw = (product.raw ?? {}) as Record<string, unknown>;
const titleZh = ((raw.title_zh as string) ?? (raw.title as string) ?? '').trim();
return (
<div style={{ display: 'flex', gap: 16 }}>
<div style={{ flex: 1, minWidth: 0, display: 'flex', flexDirection: 'column', gap: 16 }}>
<div>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<Space>
<Title level={4} style={{ margin: 0 }}>
{titleZh || product.name || '(未命名商品)'}
</Title>
<span style={{ color: STAGE_COLOR[product.stage] }}>
{STAGE_LABEL[product.stage] || product.stage}
</span>
</Space>
<Text type="secondary">{saving ? '保存中…' : '已自动保存'}</Text>
</div>
<Space size={16} style={{ marginTop: 4 }}>
{product.name && product.name !== titleZh && (
<Text type="secondary" ellipsis={{ tooltip: product.name }} style={{ maxWidth: 420 }}>
俄文:{product.name}
</Text>
)}
<Link to={`/product/${product.id}`}>进入商品编辑页 </Link>
</Space>
</div>
{/* key 随商品切换:面板本地状态(表单/方案/勾选)一次性初始化,避免保存回显互相覆盖 */}
<div id="section-info">
<Card title="1 商品信息">
<TrialInfoPanel key={`info-${product.id}`} product={product} onSave={save} />
</Card>
</div>
<div id="section-pricing">
<Card title="2 价格试算">
<TrialPricingPanel key={`pricing-${product.id}`} product={product} onSave={save} />
</Card>
</div>
<div id="section-copy">
<Card title="3 俄文文案(AI">
<CopyPanel product={product} onSave={save} leftSpan={11} rightSpan={13} />
</Card>
</div>
{/* 4/5/6 三张卡片由 TrialSuitePanel 渲染,锚点在面板内部 */}
<TrialSuitePanel product={product} assets={assets} onRefreshAssets={load} />
<div id="section-export">
<Card title="7 入库与导出">
<TrialExportPanel product={product} />
</Card>
</div>
</div>
{/* 右侧区域导航 */}
<div style={{ width: 120, flexShrink: 0 }}>
<div style={{ position: 'sticky', top: 80 }}>
<Menu
mode="inline"
selectedKeys={[active]}
style={{ borderInlineEnd: 0, background: 'transparent' }}
items={SECTIONS.map((s) => ({ key: s.id, label: s.label }))}
onClick={({ key }) => {
document.getElementById(key)?.scrollIntoView({ behavior: 'smooth', block: 'start' });
}}
/>
</div>
</div>
</div>
);
}