refactor(server): 移除鉴权并归档遗留路由至 legacy/
- 删除 auth.py 与 deps.py,各路由去除 get_current_user 依赖 - collection.py 更名为 materials.py,冻结链路(ozon/publish/shops/categories)移入 legacy/ - 扩展默认生图服务端口并入 8800 并自动迁移旧配置,水印默认文案改为 Panda Store - 新增 docs/v2.1/backend-structure.md 后端结构盘点文档
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
* 采集图片(分组勾选/上传/单张AI生图) + 出图方案(AI规划/风格/要求/模型/一键生成) + 生成结果(导出 ZIP)。
|
||||
* 交互对齐 image-suite-studio 面板 02/03/04 区块;套图服务端接口 Phase B 提供。
|
||||
*/
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useEffect, useMemo, useRef, useState, type CSSProperties } from 'react';
|
||||
import {
|
||||
Alert, Button, Card, Checkbox, Col, Empty, Image, Input, InputNumber, message, Modal, Popover, Progress,
|
||||
Radio, Row, Segmented, Select, Space, Tag, Typography, Upload,
|
||||
@@ -34,6 +34,74 @@ const GROUP_LABELS: Record<string, string> = {
|
||||
const DISPLAY_GROUPS = ['main', 'sku', 'detail', 'generated', 'upload'];
|
||||
const WATERMARK_STORAGE_KEY = 'trialWatermark';
|
||||
|
||||
/** 方案数量步进器:左减号 / 中间可输入 / 右加号(0-5) */
|
||||
function PlanStepper({ value, onChange }: { value: number; onChange: (v: number) => void }) {
|
||||
const [text, setText] = useState(String(value));
|
||||
useEffect(() => setText(String(value)), [value]);
|
||||
const clamp = (v: number) => Math.max(0, Math.min(5, Number.isNaN(v) ? 0 : v));
|
||||
const commit = () => onChange(clamp(parseInt(text, 10)));
|
||||
const btnBase: CSSProperties = {
|
||||
width: 26,
|
||||
height: 26,
|
||||
padding: 0,
|
||||
border: 'none',
|
||||
background: '#f5f5f5',
|
||||
cursor: 'pointer',
|
||||
fontSize: 15,
|
||||
lineHeight: '26px',
|
||||
color: '#555',
|
||||
};
|
||||
return (
|
||||
<span
|
||||
style={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
border: '1px solid #d9d9d9',
|
||||
borderRadius: 8,
|
||||
overflow: 'hidden',
|
||||
background: '#fff',
|
||||
}}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
title="减少"
|
||||
style={{ ...btnBase, color: value <= 0 ? '#ccc' : '#555', cursor: value <= 0 ? 'not-allowed' : 'pointer' }}
|
||||
disabled={value <= 0}
|
||||
onClick={() => onChange(clamp(value - 1))}
|
||||
>
|
||||
−
|
||||
</button>
|
||||
<input
|
||||
value={text}
|
||||
onChange={(e) => setText(e.target.value.replace(/[^\d]/g, ''))}
|
||||
onBlur={commit}
|
||||
onKeyDown={(e) => e.key === 'Enter' && (e.target as HTMLInputElement).blur()}
|
||||
title="可直接输入数量(0-5)"
|
||||
style={{
|
||||
width: 34,
|
||||
height: 26,
|
||||
textAlign: 'center',
|
||||
border: 'none',
|
||||
outline: 'none',
|
||||
fontSize: 13,
|
||||
fontWeight: 600,
|
||||
color: value === 0 ? '#bbb' : '#333',
|
||||
background: '#fff',
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
title="增加"
|
||||
style={{ ...btnBase, color: value >= 5 ? '#ccc' : '#555', cursor: value >= 5 ? 'not-allowed' : 'pointer' }}
|
||||
disabled={value >= 5}
|
||||
onClick={() => onChange(clamp(value + 1))}
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
interface Props {
|
||||
product: ProductDetail;
|
||||
assets: ProductAsset[];
|
||||
@@ -109,7 +177,9 @@ export default function TrialSuitePanel({ product, assets, onRefreshAssets }: Pr
|
||||
// 单张 AI 生图弹窗
|
||||
const [genModal, setGenModal] = useState<{ url: string; name: string } | null>(null);
|
||||
|
||||
const imgs = useMemo(() => assets.filter((a) => a.type !== 'video'), [assets]);
|
||||
// 采集图片卡只放采集/上传素材;单张 AI 生图结果(generated)放「生成结果」卡展示
|
||||
const imgs = useMemo(() => assets.filter((a) => a.type !== 'video' && a.group_key !== 'generated'), [assets]);
|
||||
const generatedAssets = useMemo(() => assets.filter((a) => a.group_key === 'generated'), [assets]);
|
||||
const groupImages = (g: string) => imgs.filter((a) => a.group_key === g);
|
||||
const assetUrl = (a: ProductAsset) => a.stored_url || a.source_url;
|
||||
const rawObj = (product.raw ?? {}) as Record<string, unknown>;
|
||||
@@ -597,7 +667,6 @@ export default function TrialSuitePanel({ product, assets, onRefreshAssets }: Pr
|
||||
规划并生成
|
||||
</Checkbox>
|
||||
<Button
|
||||
size="small"
|
||||
type="primary"
|
||||
ghost
|
||||
icon={<ThunderboltOutlined />}
|
||||
@@ -639,13 +708,7 @@ export default function TrialSuitePanel({ product, assets, onRefreshAssets }: Pr
|
||||
</Space>
|
||||
</div>
|
||||
<span onClick={(e) => e.stopPropagation()}>
|
||||
<InputNumber
|
||||
size="small"
|
||||
min={0}
|
||||
max={5}
|
||||
value={p.count}
|
||||
onChange={(v) => setPlanCount(idx, v ?? 0)}
|
||||
/>
|
||||
<PlanStepper value={p.count} onChange={(v) => setPlanCount(idx, v)} />
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
@@ -670,18 +733,10 @@ export default function TrialSuitePanel({ product, assets, onRefreshAssets }: Pr
|
||||
恢复默认方案
|
||||
</a>
|
||||
)}
|
||||
<Text type="secondary" style={{ fontSize: 12, flex: 1 }} ellipsis={{ tooltip: planSummary }}>
|
||||
<Text type="secondary" style={{ fontSize: 12, flex: 1, whiteSpace: 'normal', wordBreak: 'break-all' }}>
|
||||
{planSummary || '方案与张数由规划器按商品信息自动决定,可手动微调,0 即不生成'}
|
||||
</Text>
|
||||
</div>
|
||||
{/* 水印设置:左列最下、靠右 */}
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end', marginTop: 8 }}>
|
||||
<Popover trigger="click" placement="bottomRight" content={watermarkPopup} title="水印设置">
|
||||
<Button size="small" icon={<SettingOutlined />}>
|
||||
水印
|
||||
</Button>
|
||||
</Popover>
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
@@ -731,17 +786,7 @@ export default function TrialSuitePanel({ product, assets, onRefreshAssets }: Pr
|
||||
placeholder="选填,例如:必须保留商品正面品牌标识;背景必须为纯黑色;不得添加任何文字水印"
|
||||
/>
|
||||
</div>
|
||||
<Row align="middle" style={{ marginTop: 12, gap: 12 }} wrap={false}>
|
||||
{generating && (
|
||||
<div style={{ flex: 1, minWidth: 200 }}>
|
||||
<Progress
|
||||
percent={suiteTotal ? Math.round((doneCount / suiteTotal) * 100) : 0}
|
||||
size={['100%', 10]}
|
||||
status="active"
|
||||
format={() => `${doneCount}/${suiteTotal}`}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<Row align="middle" style={{ marginTop: 12, gap: 12 }} justify="end" wrap={false}>
|
||||
<Select
|
||||
style={{ width: 260 }}
|
||||
popupMatchSelectWidth={false}
|
||||
@@ -766,6 +811,15 @@ export default function TrialSuitePanel({ product, assets, onRefreshAssets }: Pr
|
||||
{generating ? '生成中…' : `一键生图(${totalPlanned} 张)`}
|
||||
</Button>
|
||||
</Row>
|
||||
{generating && (
|
||||
<Progress
|
||||
style={{ marginTop: 12 }}
|
||||
percent={suiteTotal ? Math.round((doneCount / suiteTotal) * 100) : 0}
|
||||
size={['100%', 10]}
|
||||
status="active"
|
||||
format={() => `${doneCount}/${suiteTotal}`}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
@@ -777,17 +831,27 @@ export default function TrialSuitePanel({ product, assets, onRefreshAssets }: Pr
|
||||
<Card
|
||||
title="6 生成结果"
|
||||
extra={
|
||||
suite && ['done', 'partial'].includes(suite.status) && (
|
||||
<Button size="small" icon={<DownloadOutlined />} loading={exportingZip} onClick={handleExportZip}>
|
||||
导出 ZIP
|
||||
</Button>
|
||||
)
|
||||
<Space>
|
||||
{suite && ['done', 'partial'].includes(suite.status) && (
|
||||
<Button size="small" icon={<DownloadOutlined />} loading={exportingZip} onClick={handleExportZip}>
|
||||
导出 ZIP
|
||||
</Button>
|
||||
)}
|
||||
{/* 水印设置(作用于下次生成) */}
|
||||
<Popover trigger="click" placement="bottomRight" content={watermarkPopup} title="水印设置">
|
||||
<Button size="small" icon={<SettingOutlined />}>
|
||||
水印
|
||||
</Button>
|
||||
</Popover>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
{!suite ? (
|
||||
{!suite && generatedAssets.length === 0 ? (
|
||||
<Empty description="生成后在此查看与导出(目标规格:俄文文案 · 3:4 图片)" />
|
||||
) : (
|
||||
<>
|
||||
{suite && (
|
||||
<>
|
||||
<div style={{ marginBottom: 8 }}>
|
||||
<Space wrap size={8}>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
@@ -869,6 +933,47 @@ export default function TrialSuitePanel({ product, assets, onRefreshAssets }: Pr
|
||||
</div>
|
||||
</Image.PreviewGroup>
|
||||
{suite.error && <Alert type="warning" showIcon message={suite.error} style={{ marginTop: 8 }} />}
|
||||
</>
|
||||
)}
|
||||
{/* 单张 AI 生图结果(采集图片区「AI 生图」生成,回写 generated 组) */}
|
||||
{generatedAssets.length > 0 && (
|
||||
<div style={suite ? { borderTop: '1px dashed #e5e7eb', paddingTop: 12, marginTop: 14 } : {}}>
|
||||
<Text strong>
|
||||
单张 AI 生图
|
||||
<Text type="secondary" style={{ fontWeight: 400, fontSize: 11, marginLeft: 8 }}>
|
||||
(采集图片区「AI 生图」的结果 · {generatedAssets.length} 张)
|
||||
</Text>
|
||||
</Text>
|
||||
<Image.PreviewGroup>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 10, marginTop: 10 }}>
|
||||
{generatedAssets.map((a) => (
|
||||
<div key={a.id} style={{ width: 112 }}>
|
||||
<Image
|
||||
src={assetUrl(a)}
|
||||
width={100}
|
||||
height={133}
|
||||
style={{ objectFit: 'cover', borderRadius: 6 }}
|
||||
fallback="data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='100' height='133'><rect width='100' height='133' fill='%23eee'/><text x='18' y='70' font-size='11' fill='%23999'>无预览</text></svg>"
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 11,
|
||||
color: 'rgba(0,0,0,0.45)',
|
||||
marginTop: 2,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
title={a.stored_url ?? undefined}
|
||||
>
|
||||
AI 生图 {a.created_at ? new Date(a.created_at).toLocaleTimeString() : ''}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Image.PreviewGroup>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
Reference in New Issue
Block a user