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:
R524809
2026-08-28 15:09:22 +08:00
parent 2835914fd8
commit dc6d38c128
44 changed files with 1556 additions and 389 deletions
+141 -36
View File
@@ -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>