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:
@@ -1,16 +0,0 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router';
|
||||
import { getToken } from '@/services/auth';
|
||||
|
||||
/** 未登录则跳 /login */
|
||||
export default function RequireAuth({ children }: { children: React.ReactNode }) {
|
||||
const navigate = useNavigate();
|
||||
const token = getToken();
|
||||
|
||||
useEffect(() => {
|
||||
if (!token) navigate('/login', { replace: true });
|
||||
}, [token, navigate]);
|
||||
|
||||
if (!token) return null;
|
||||
return <>{children}</>;
|
||||
}
|
||||
@@ -31,7 +31,8 @@ const MainLayout = () => {
|
||||
{!isMobile && (
|
||||
<Sider
|
||||
className="main-sider"
|
||||
width={240}
|
||||
width={200}
|
||||
collapsedWidth={60}
|
||||
collapsed={collapsed}
|
||||
theme="dark"
|
||||
style={{
|
||||
@@ -58,7 +59,7 @@ const MainLayout = () => {
|
||||
onClose={() => setMobileMenuOpen(false)}
|
||||
open={mobileMenuOpen}
|
||||
styles={{ body: { padding: 0, background: 'var(--sider-bg)' } }}
|
||||
width={240}
|
||||
width={200}
|
||||
>
|
||||
<SidebarMenu collapsed={false} onMenuClick={() => setMobileMenuOpen(false)} />
|
||||
</Drawer>
|
||||
@@ -67,7 +68,7 @@ const MainLayout = () => {
|
||||
<Layout
|
||||
className="main-content-layout"
|
||||
style={{
|
||||
marginLeft: isMobile ? 0 : collapsed ? 80 : 240,
|
||||
marginLeft: isMobile ? 0 : collapsed ? 60 : 200,
|
||||
transition: 'margin-left 0.2s',
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -80,3 +80,55 @@
|
||||
padding: 14px 20px !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── 折叠态(60px):图标居中,覆盖展开态的左对齐 padding/位移 ── */
|
||||
.sidebar-menu.ant-menu-inline-collapsed .ant-menu-item,
|
||||
.sidebar-menu.ant-menu-inline-collapsed .ant-menu-item-group-title {
|
||||
padding: 12px 0 !important;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.sidebar-menu.ant-menu-inline-collapsed .ant-menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sidebar-menu.ant-menu-inline-collapsed .ant-menu-item .ant-menu-title-content,
|
||||
.sidebar-menu.ant-menu-inline-collapsed .ant-menu-item-selected .ant-menu-title-content {
|
||||
transform: none !important;
|
||||
}
|
||||
|
||||
.sidebar-menu.ant-menu-inline-collapsed .ant-menu-item .ant-menu-item-icon,
|
||||
.sidebar-menu.ant-menu-inline-collapsed .ant-menu-item:hover .ant-menu-item-icon,
|
||||
.sidebar-menu.ant-menu-inline-collapsed .ant-menu-item-selected .ant-menu-item-icon {
|
||||
transform: none !important;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* 折叠时去掉左侧 3px 选中边框(会挤偏图标),选中态用高亮背景区分 */
|
||||
.sidebar-menu.ant-menu-inline-collapsed .ant-menu-item-selected {
|
||||
border-left: none !important;
|
||||
}
|
||||
|
||||
/* 折叠时隐藏 label 占位(antd 折叠动画保留 opacity 占位,会把图标挤离中心) */
|
||||
.sidebar-menu.ant-menu-inline-collapsed .ant-menu-item .ant-menu-title-content {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* 兼容:部分版本折叠态用 vertical 类渲染,同样居中 */
|
||||
.sidebar-menu.ant-menu-vertical .ant-menu-item,
|
||||
.sidebar-menu.ant-menu-inline-collapsed .ant-menu-item {
|
||||
padding-left: 0 !important;
|
||||
padding-right: 0 !important;
|
||||
}
|
||||
|
||||
.sidebar-menu.ant-menu-vertical .ant-menu-item .ant-menu-title-content {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* 折叠态 flex 子元素居中(display:flex 已生效,缺 justify-content) */
|
||||
.sidebar-menu.ant-menu-inline-collapsed .ant-menu-item {
|
||||
justify-content: center !important;
|
||||
}
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
import { useNavigate } from 'react-router';
|
||||
import { Button, Card, Input, message, Typography } from 'antd';
|
||||
import { login } from '@/services/auth';
|
||||
import { apiErrorMessage } from '@/services/api';
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
|
||||
export default function LoginPage() {
|
||||
const navigate = useNavigate();
|
||||
const [token, setToken] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const onSubmit = async () => {
|
||||
if (!token.trim()) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
await login(token.trim());
|
||||
message.success('登录成功');
|
||||
navigate('/collection', { replace: true });
|
||||
} catch (e) {
|
||||
message.error(apiErrorMessage(e));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ minHeight: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center', background: '#f5f5f5' }}>
|
||||
<Card style={{ width: 420 }}>
|
||||
<Title level={3} style={{ marginTop: 0 }}>Ozon 发布工作台</Title>
|
||||
<Text type="secondary">请输入访问 Token(对应服务端 .env 的 APP_TOKEN)</Text>
|
||||
<Input.Password
|
||||
placeholder="APP_TOKEN"
|
||||
value={token}
|
||||
onChange={(e) => setToken(e.target.value)}
|
||||
onPressEnter={onSubmit}
|
||||
style={{ marginTop: 16 }}
|
||||
/>
|
||||
<Button type="primary" block loading={loading} onClick={onSubmit} style={{ marginTop: 16 }}>
|
||||
登录
|
||||
</Button>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState } from 'react';
|
||||
import { Col, Input, message, Row, Typography } from 'antd';
|
||||
import { Button, Col, Input, message, Row, Typography } from 'antd';
|
||||
import { CopyOutlined, ExportOutlined } from '@ant-design/icons';
|
||||
import { ProductDetail } from '@/services/product';
|
||||
import { copyText } from '@/utils/file';
|
||||
@@ -27,7 +27,15 @@ export default function TrialInfoPanel({ product, onSave }: Props) {
|
||||
const [titleZh, setTitleZh] = useState(((raw.title_zh as string) ?? (raw.title as string) ?? '').trim());
|
||||
const [nameRu, setNameRu] = useState(product.name ?? '');
|
||||
const [modelCode, setModelCode] = useState((raw.model_code as string) ?? '');
|
||||
const [offerId, setOfferId] = useState(product.offer_id ?? '');
|
||||
// 货号 = 型号-后缀:后缀独立存储(对齐 v1 modelCode+skuSuffix),型号只做前缀拼接、永不反推
|
||||
const [skuSuffix, setSkuSuffix] = useState(() => {
|
||||
const model = ((raw.model_code as string) ?? '').trim();
|
||||
const offer = (product.offer_id ?? '').trim();
|
||||
if (!offer) return '';
|
||||
return model && offer.startsWith(`${model}-`) ? offer.slice(model.length + 1) : offer;
|
||||
});
|
||||
// 完整货号:型号与后缀拼接(任一为空则省略对应段与「-」)
|
||||
const fullSku = [modelCode.trim(), skuSuffix.trim()].filter(Boolean).join('-');
|
||||
// 采买地址:仅 1688/拼多多/淘宝/天猫 来源时用 source_url 初始化
|
||||
const [purchaseUrl, setPurchaseUrl] = useState(
|
||||
(raw.purchase_url as string) ?? (isPurchasePlatform(product.source_platform) ? (product.source_url ?? '') : ''),
|
||||
@@ -38,19 +46,9 @@ export default function TrialInfoPanel({ product, onSave }: Props) {
|
||||
|
||||
const params = Array.isArray(raw.params) ? (raw.params as Array<{ key: string; value: string }>) : [];
|
||||
|
||||
/** 型号变化:货号前缀始终同步为新型号(对齐 v1 web:货号 = 型号-后缀)。
|
||||
* 货号为空 → 带入「型号-」;货号非空 → 替换第一个「-」前的前缀、保留后缀;
|
||||
* 型号清空时货号保持不动(避免误删已填后缀)。 */
|
||||
const onModelChange = (v: string) => {
|
||||
setModelCode(v);
|
||||
setOfferId((prev) => {
|
||||
if (!prev) return v ? `${v}-` : '';
|
||||
if (!v) return prev;
|
||||
const idx = prev.indexOf('-');
|
||||
const suffix = idx >= 0 ? prev.slice(idx) : '-';
|
||||
return `${v}${suffix}`;
|
||||
});
|
||||
};
|
||||
/** 型号/后缀变化后同步完整货号落库 */
|
||||
const saveSku = (model: string, suffix: string) =>
|
||||
onSave({ offer_id: [model.trim(), suffix.trim()].filter(Boolean).join('-') });
|
||||
|
||||
const openUrl = purchaseUrl?.trim()
|
||||
? `https://${purchaseUrl.trim().replace(/^https?:\/\//, '')}`
|
||||
@@ -85,17 +83,46 @@ export default function TrialInfoPanel({ product, onSave }: Props) {
|
||||
<Input
|
||||
value={modelCode}
|
||||
placeholder="如 YZ"
|
||||
onChange={(e) => onModelChange(e.target.value)}
|
||||
onBlur={() => patchRaw({ model_code: modelCode.trim() })}
|
||||
onChange={(e) => {
|
||||
setModelCode(e.target.value);
|
||||
saveSku(e.target.value, skuSuffix);
|
||||
}}
|
||||
addonAfter={
|
||||
<a
|
||||
title="复制型号"
|
||||
style={{ opacity: modelCode.trim() ? 1 : 0.35 }}
|
||||
onClick={() => {
|
||||
if (!modelCode.trim()) return;
|
||||
copyText(modelCode.trim()).then((ok) => ok && message.success('已复制型号'));
|
||||
}}
|
||||
>
|
||||
<CopyOutlined />
|
||||
</a>
|
||||
}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<FieldLabel>货号(SKU)</FieldLabel>
|
||||
<FieldLabel>货号(SKU){modelCode.trim() && <Text type="secondary" style={{ fontSize: 11, fontWeight: 400 }}>= 型号-后缀</Text>}</FieldLabel>
|
||||
<Input
|
||||
value={offerId}
|
||||
placeholder="型号-后缀(型号自动带入前缀)"
|
||||
onChange={(e) => setOfferId(e.target.value)}
|
||||
onBlur={() => onSave({ offer_id: offerId.trim() })}
|
||||
value={skuSuffix}
|
||||
placeholder="后缀,如 001"
|
||||
addonBefore={modelCode.trim() ? `${modelCode.trim()}-` : undefined}
|
||||
onChange={(e) => {
|
||||
setSkuSuffix(e.target.value);
|
||||
saveSku(modelCode, e.target.value);
|
||||
}}
|
||||
addonAfter={
|
||||
<a
|
||||
title="复制完整货号"
|
||||
style={{ opacity: fullSku ? 1 : 0.35 }}
|
||||
onClick={() => {
|
||||
if (!fullSku) return;
|
||||
copyText(fullSku).then((ok) => ok && message.success(`已复制货号:${fullSku}`));
|
||||
}}
|
||||
>
|
||||
<CopyOutlined />
|
||||
</a>
|
||||
}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
@@ -108,11 +135,17 @@ export default function TrialInfoPanel({ product, onSave }: Props) {
|
||||
onChange={(e) => setPurchaseUrl(e.target.value)}
|
||||
onBlur={() => patchRaw({ purchase_url: purchaseUrl.trim() })}
|
||||
addonAfter={
|
||||
<span style={{ display: 'inline-flex', gap: 10 }}>
|
||||
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 8 }}>
|
||||
{openUrl && (
|
||||
<a href={openUrl} target="_blank" rel="noreferrer" title="打开采买地址">
|
||||
<ExportOutlined />
|
||||
</a>
|
||||
<Button
|
||||
size="small"
|
||||
type="primary"
|
||||
ghost
|
||||
icon={<ExportOutlined />}
|
||||
onClick={() => window.open(openUrl, '_blank', 'noopener')}
|
||||
>
|
||||
打开地址
|
||||
</Button>
|
||||
)}
|
||||
<a title="复制采买地址" onClick={copyPurchaseUrl}>
|
||||
<CopyOutlined />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -20,6 +20,5 @@ export const router = createBrowserRouter([
|
||||
],
|
||||
},
|
||||
// 后续加账户体系时再启用 /login
|
||||
{ path: '/login', element: <Navigate to="/collection" replace /> },
|
||||
{ path: '*', element: <Navigate to="/" replace /> },
|
||||
]);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import axios from 'axios';
|
||||
import type { AxiosInstance, AxiosRequestConfig } from 'axios';
|
||||
import { envConfig } from '@/config/env';
|
||||
import { getToken } from './auth';
|
||||
|
||||
const apiClient: AxiosInstance = axios.create({
|
||||
baseURL: envConfig.apiBaseUrl,
|
||||
@@ -11,16 +10,6 @@ const apiClient: AxiosInstance = axios.create({
|
||||
},
|
||||
});
|
||||
|
||||
// 请求拦截:附带 JWT
|
||||
apiClient.interceptors.request.use((config) => {
|
||||
const token = getToken();
|
||||
if (token) {
|
||||
config.headers = config.headers ?? {};
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
return config;
|
||||
});
|
||||
|
||||
apiClient.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error) => {
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
import { api } from './api';
|
||||
|
||||
const TOKEN_KEY = 'ozon_kit_jwt';
|
||||
|
||||
export function getToken(): string | null {
|
||||
return localStorage.getItem(TOKEN_KEY);
|
||||
}
|
||||
|
||||
export function setToken(token: string) {
|
||||
localStorage.setItem(TOKEN_KEY, token);
|
||||
}
|
||||
|
||||
export function clearToken() {
|
||||
localStorage.removeItem(TOKEN_KEY);
|
||||
}
|
||||
|
||||
export interface LoginResult {
|
||||
access_token: string;
|
||||
token_type: string;
|
||||
expires_at: number;
|
||||
}
|
||||
|
||||
export async function login(appToken: string): Promise<LoginResult> {
|
||||
const res = await api.post<LoginResult>('/auth/login', { token: appToken });
|
||||
setToken(res.access_token);
|
||||
return res;
|
||||
}
|
||||
|
||||
export function logout() {
|
||||
clearToken();
|
||||
}
|
||||
@@ -67,6 +67,7 @@ export interface ProductAsset {
|
||||
width: number | null;
|
||||
height: number | null;
|
||||
error: string | null;
|
||||
created_at?: string;
|
||||
}
|
||||
|
||||
export function listProducts(params?: { stage?: string; q?: string; page?: number; page_size?: number }) {
|
||||
|
||||
@@ -80,7 +80,7 @@ export interface WatermarkPayload {
|
||||
export const DEFAULT_WATERMARK: WatermarkPayload = {
|
||||
enabled: false,
|
||||
type: 'text',
|
||||
text: 'xiongmaoyx',
|
||||
text: 'Panda Store',
|
||||
opacity: 30,
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user