diff --git a/core/backend/apps/ai/providers/volcano.py b/core/backend/apps/ai/providers/volcano.py index 9f66f31..fd9b23e 100644 --- a/core/backend/apps/ai/providers/volcano.py +++ b/core/backend/apps/ai/providers/volcano.py @@ -176,7 +176,9 @@ class VolcanoArkProvider: json=body, timeout=180, ) - response.raise_for_status() + # 非 2xx 抽出火山真实报错(error.code/message)而非裸 HTTP 400 —— 否则 Seedream 的 + # 内容审核 / 尺寸过小(历史 4:5 套图 raise_for_status 吞 body 的踩坑)等真因看不见。 + _raise_volcano_error(response) return response.json() def create_video_task( diff --git a/core/frontend/src/ai-tools-page.css b/core/frontend/src/ai-tools-page.css index 15a0a59..673e136 100644 --- a/core/frontend/src/ai-tools-page.css +++ b/core/frontend/src/ai-tools-page.css @@ -627,6 +627,11 @@ .image-workbench .p-logo-sph { background: #07c160; } .image-workbench .p-logo-amz { background: #ff9900; } .image-workbench .p-logo-al { background: #2c4af1; } +/* YYX#row10:真平台 logo 图 —— 填满 logo 容器(平台卡 32 / 筛选 20 / 分组头 24); + has-img 时容器底色改中性,真 logo 自带配色覆盖在上;图加载失败回退品牌色块字符 */ +.image-workbench .p-logo.has-img, +.image-workbench .cg-logo.has-img { background: var(--surface); overflow: hidden; } +.image-workbench .p-logo-img { width: 100%; height: 100%; object-fit: cover; border-radius: inherit; display: block; } /* ── 提示词框(YYX#7 加大)+ 框内生图模型胶囊(YYX#10 靠左置底)── */ .image-workbench .iw-prompt-box { position: relative; } diff --git a/core/frontend/src/components/overlays.tsx b/core/frontend/src/components/overlays.tsx index ccff651..fde9e55 100644 --- a/core/frontend/src/components/overlays.tsx +++ b/core/frontend/src/components/overlays.tsx @@ -100,7 +100,7 @@ export function SettingRow({ title, desc, action, toggle, checked }: { title: st ); } -export function TeamModal({ open, title, subtitle, icon, close, children, footer }: { +export function TeamModal({ open, title, subtitle, icon, close, children, footer, dismissable = true }: { open: boolean; title: string; subtitle: string; @@ -108,13 +108,15 @@ export function TeamModal({ open, title, subtitle, icon, close, children, footer close: () => void; children: ReactNode; footer?: ReactNode; + /** 是否允许点击遮罩关闭弹窗。默认 true(保持原有行为);传 false 则点遮罩不响应。 */ + dismissable?: boolean; }) { useBodyScrollLock(open); const { mounted, show } = useOverlayTransition(open, close); if (!mounted) return null; // 挂到 body:脱离 .content(z-index:1)层叠上下文,遮罩才能盖住头部/侧栏 return createPortal( -
+
event.stopPropagation()}> ++
{icon}
{title}{subtitle}
diff --git a/core/frontend/src/messages-page.css b/core/frontend/src/messages-page.css index dedcb80..fe22b94 100644 --- a/core/frontend/src/messages-page.css +++ b/core/frontend/src/messages-page.css @@ -181,7 +181,9 @@ .msg-type-ic.team { background: var(--black-alpha-4); color: var(--accent-black); } .msg-type-ic.billing { background: var(--honey-bg); border-color: var(--honey-bd); color: var(--accent-honey); } .msg-type-ic.system { background: var(--black-alpha-7); color: var(--black-alpha-72); } -.msg-item-main { min-width: 0; } +/* display:block 将 span 提升为块级容器,确保 flex 子行(.msg-item-row/.msg-brief/.msg-item-foot) + 能正确换行堆叠;否则父 inline-span 与 grid 结合时文字在框内错位 */ +.msg-item-main { min-width: 0; display: block; } .msg-item-row { display: flex; align-items: center; diff --git a/core/frontend/src/products-page.css b/core/frontend/src/products-page.css index 4eeb3d3..76e0c1d 100644 --- a/core/frontend/src/products-page.css +++ b/core/frontend/src/products-page.css @@ -112,3 +112,28 @@ body.edit-mode .bulk-bar { display: inline-flex; } .pd-role-foot .mono { font-size: 11px; color: var(--black-alpha-48); letter-spacing: .02em; } /* 角色卡「含三视图」小标(右下角) */ .pd-role-tri { position: absolute; right: 8px; bottom: 8px; padding: 1px 7px; border-radius: var(--r-pill); background: var(--black-alpha-56); color: var(--accent-white); font-size: 10.5px; letter-spacing: .02em; } + +/* ── 商品详情·商品图片网格·编辑态悬停删除按钮 ── + .img-del-btn:悬停在可删图片上时显示垃圾桶图标按钮(右上角), + 点整张图仍走放大预览,只有点垃圾桶才触发删除,避免误操作。 + 使用 design token:8px 圆角、单橙 hover、无裸 hex。 + 同时覆盖 product-detail-page.css 里遗留的 ::after 叉号(已换成真实按钮,隐藏旧伪元素)。 */ +.ov-card.editing .ov-images-sub .thumb.is-del::after { display: none; } +.ov-images-sub .thumb.is-del { position: relative; } +.ov-images-sub .thumb.is-del .img-del-btn { + position: absolute; + top: 5px; right: 5px; + width: 26px; height: 26px; + display: none; + align-items: center; justify-content: center; + background: rgba(0,0,0,.60); + border: 0; + border-radius: var(--r-sm); + color: var(--accent-white); + cursor: pointer; + z-index: 4; + transition: background var(--t-base); + padding: 0; +} +.ov-images-sub .thumb.is-del:hover .img-del-btn { display: flex; } +.ov-images-sub .thumb.is-del .img-del-btn:hover { background: var(--heat); } diff --git a/core/frontend/src/routes/ai-tools.tsx b/core/frontend/src/routes/ai-tools.tsx index 209c810..9a63dd7 100644 --- a/core/frontend/src/routes/ai-tools.tsx +++ b/core/frontend/src/routes/ai-tools.tsx @@ -550,20 +550,34 @@ const FALLBACK_MODELS = [ { id: "m4", name: "Zoe", tag: "亚洲·30岁·健身" } ]; -/* 平台套图 · 平台卡(基线 platform-cover · logo 配色用 token 化 className) */ +/* 平台套图 · 平台卡(YYX#row10:用真平台 logo,素材在 public/assets/svg; + logo 留作图加载失败时的兜底字符,img 为真 logo) */ const PLATFORM_OPTIONS = [ - { id: "dy", name: "抖音电商", logo: "抖" }, - { id: "tb", name: "淘宝", logo: "淘" }, - { id: "tm", name: "天猫", logo: "猫" }, - { id: "jd", name: "京东", logo: "京" }, - { id: "pdd", name: "拼多多", logo: "拼" }, - { id: "xhs", name: "小红书", logo: "红" }, - { id: "ks", name: "快手", logo: "快" }, - { id: "sph", name: "视频号", logo: "视" }, - { id: "amz", name: "亚马逊", logo: "a" }, - { id: "al", name: "1688", logo: "阿" } + { id: "dy", name: "抖音电商", logo: "抖", img: "/assets/svg/icon-platform-douyin.svg" }, + { id: "tb", name: "淘宝", logo: "淘", img: "/assets/svg/icon-platform-taobao.svg" }, + { id: "tm", name: "天猫", logo: "猫", img: "/assets/svg/icon-platform-tmall.svg" }, + { id: "jd", name: "京东", logo: "京", img: "/assets/svg/icon-platform-jd.svg" }, + { id: "pdd", name: "拼多多", logo: "拼", img: "/assets/svg/icon-platform-pdd.svg" }, + { id: "xhs", name: "小红书", logo: "红", img: "/assets/svg/icon-platform-xiaohongshu.svg" }, + { id: "ks", name: "快手", logo: "快", img: "/assets/svg/icon-platform-kuaishou.svg" }, + { id: "sph", name: "视频号", logo: "视", img: "/assets/svg/icon-platform-wechat-video.svg" }, + { id: "amz", name: "亚马逊", logo: "a", img: "/assets/svg/icon-platform-amazon.svg" }, + { id: "al", name: "1688", logo: "阿", img: "/assets/svg/icon-platform-1688.svg" } ]; +/* YYX#row10:平台 logo —— 优先真 logo 图(public/assets/svg),加载失败回退到品牌色块+字符。 + className 默认 p-logo(平台卡/筛选弹窗),分组头传 cg-logo。 */ +function PlatformLogo({ p, className }: { p: { id: string; name: string; logo: string; img?: string }; className?: string }) { + const [err, setErr] = useState(false); + return ( + + {p.img && !err + ? {p.name} setErr(true)} /> + : p.logo} + + ); +} + /* 一次生成/重跑 = 一个批次:各自独立展示自己的图、状态、加入资产库切换;支持多批并行跑。 行31(并发提交) / 行32(重跑追加新批次 + 气泡菜单 + 加入/取消切换) / 行33(多批次持久化恢复)。 */ type GenBatch = { @@ -784,6 +798,9 @@ export function ImageWorkbenchPage({ const ratioVar = ratio.replace(":", " / "); const candidateCount = Math.max(1, Number(count) || 4); + // PMC#5:平台套图(cover)对每个选中平台各起一批,实扣 = 平台数 × candidateCount × 单价。 + // 预估必须乘平台数才与实扣口径一致(model/image 模式只生成 candidateCount 张,不乘)。 + const estimateCount = mode === "cover" ? Math.max(1, pickedIds.length) * candidateCount : candidateCount; // 行31:并发提交——只要 prompt 非空就能再次提交,不再因"有批次在跑"被禁用 // 模特上身图需结合「商品图 + 模特图」合成,故必须先选商品且选一个模特,否则只是凭文字脑补 const canGenerate = @@ -1412,7 +1429,7 @@ export function ImageWorkbenchPage({
{plat ? ( <> - {plat.logo} + {plat.name} ) : ( @@ -1778,7 +1795,7 @@ export function ImageWorkbenchPage({ className={`iw-filter-opt${platformFilter.includes(p.id) ? " on" : ""}`} onClick={() => setPlatformFilter((prev) => (prev.includes(p.id) ? prev.filter((x) => x !== p.id) : [...prev, p.id]))} > - {p.logo} + {p.name} @@ -1894,7 +1911,7 @@ export function ImageWorkbenchPage({ onClick={() => togglePick(item.id)} > - {item.logo} + {item.name} ))} @@ -2012,7 +2029,7 @@ export function ImageWorkbenchPage({ {/* 行31:并发提交——有批次在跑时仍可再次提交,只在提交瞬间不重复触发 */} {/* YYX#9:去掉生成按钮下方那行说明文字 */}
diff --git a/core/frontend/src/routes/dashboard.tsx b/core/frontend/src/routes/dashboard.tsx index b97c591..19317fb 100644 --- a/core/frontend/src/routes/dashboard.tsx +++ b/core/frontend/src/routes/dashboard.tsx @@ -4,10 +4,32 @@ import type { Page } from "./route-config"; import { api } from "../api"; import { SkeletonRows } from "../components/loading"; import { money, stageMeta, statusPill } from "./stage-config"; -import { Progress } from "../components/pipeline-stage"; import { IconKitSvg } from "../components/IconKitSvg"; import { ProductCreateDrawer, type ProductCreatePayload } from "../components/product-create-drawer"; +// 工作台「最近项目」进度条:4 段(与视频项目菜单一致) +// script→1 / base_assets→2 / storyboard→3 / video|export→4 +// completed:全 done(绿);failed:当前格 fail(红),之前 done;其余:之前 done,当前 cur(橙闪) +const DASH_STAGE_TOTAL = 4; +const DASH_STAGE_NO: Record = { script: 1, base_assets: 2, storyboard: 3, video: 4, export: 4 }; +function dashProgClass(project: Project, i: number, no: number): string { + if (project.status === "completed") return i <= no ? "done" : ""; + if (project.status === "failed") return i === no ? "fail" : i < no ? "done" : ""; + return i < no ? "done" : i === no ? "cur" : ""; +} +function DashProgress({ project }: { project: Project }) { + const no = project.status === "completed" + ? DASH_STAGE_TOTAL + : (DASH_STAGE_NO[project.current_stage] || 1); + return ( +
+ {Array.from({ length: DASH_STAGE_TOTAL }, (_, k) => k + 1).map((i) => ( + + ))} +
+ ); +} + export function Dashboard({ products, projects, productTotal, projectTotal, billing, userName, loading = false, navigate, onCreateProduct }: { products: Product[]; projects: Project[]; @@ -74,7 +96,7 @@ export function Dashboard({ products, projects, productTotal, projectTotal, bill
++
-

最近项目

{loading && projects.length === 0 ? : projects.slice(0, 6).map((project) => )}

快捷入口

[ /shortcuts ]
navigate("products")} /> navigate("library")} /> navigate("account")} /> navigate("projects")} />

提示

[ FAQ ]
扣费规则生成失败、超时、用户重跑 — 均不扣费。仅在你点 [ 确认通过 ] 时按 token 实际结算。
+

最近项目

{loading && projects.length === 0 ? : projects.slice(0, 6).map((project) => )}

快捷入口

[ /shortcuts ]
navigate("products")} /> navigate("library")} /> navigate("account")} /> navigate("projects")} />

提示

[ FAQ ]
扣费规则生成失败、超时、用户重跑 — 均不扣费。仅在你点 [ 确认通过 ] 时按 token 实际结算。
{/* YYX#11:新建商品抽屉 · 复用商品库同一组件,创建成功后跳商品库看新商品 */} {onCreateProduct && ( >(new Set()); + // 当 product.images 从父组件刷新进来时,已删除的 key 在服务端确认后不再存在,清理本地标记 + // eslint-disable-next-line react-hooks/exhaustive-deps + const productImageKeys = productImages.map((im) => im.key).join(","); + useEffect(() => { + setDeletedImageKeys((prev) => { + if (prev.size === 0) return prev; + // 保留仍在 productImages 里的 key(说明服务端还没确认),移除已经不在的 key(服务端已确认删除) + const stillExist = new Set(productImages.map((im) => im.key)); + const next = new Set([...prev].filter((k) => stillExist.has(k))); + return next.size === prev.size ? prev : next; + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [productImageKeys]); + + const visibleProductImages = productImages.filter((im) => !deletedImageKeys.has(im.key)); // 可删的图(有 ProductImage 关联)数量:只剩一张时锁住最后一张,不允许删到没图 - const deletableImageCount = productImages.filter((im) => im.imageId).length; + const deletableImageCount = visibleProductImages.filter((im) => im.imageId).length; // AI 生成素材 · 服务端已按 ?product 把「该商品」的素材(metadata.product_id / origin_task→project→product / // ProductImage 关联)懒加载到 productAssets;这里只按可显示的图片类型过滤。 @@ -978,21 +996,45 @@ export function ProductDetailPage({ product, projects, initialTab = "assets", na ({productImages.length})
- {productImages.map((image) => { - // 商品至少保留一张图:只剩一张可删图时,那张不显示叉子、不可删(要移除请删整个商品) + {visibleProductImages.map((image) => { + // 商品至少保留一张图:只剩一张可删图时,那张锁住不可删(要移除请删整个商品) const canDelete = editing && Boolean(image.imageId) && deletableImageCount > 1; const lastLocked = editing && Boolean(image.imageId) && deletableImageCount <= 1; - const onThumb = () => { - if (canDelete) void onDeleteImage?.(image.imageId as string); - else if (image.url) setPreview({ src: image.url, name: realName }); + const handleDelete = async (event: { stopPropagation: () => void }) => { + event.stopPropagation(); + if (!canDelete || !image.imageId || !onDeleteImage) return; + // 乐观隐藏:立刻从本地列表移除,toast/loadData 并行回填 + setDeletedImageKeys((prev) => new Set([...prev, image.key])); + const result = await onDeleteImage(image.imageId); + if (result === null) { + // action 返回 null 表示调用失败(会弹错误 toast),回滚乐观隐藏 + setDeletedImageKeys((prev) => { const next = new Set(prev); next.delete(image.key); return next; }); + } }; return ( -
{ if (event.key === "Enter" || event.key === " ") { event.preventDefault(); onThumb(); } }}> +
{ if (image.url) setPreview({ src: image.url, name: realName }); }} + onKeyDown={(event) => { if (event.key === "Enter" || event.key === " ") { event.preventDefault(); if (image.url) setPreview({ src: image.url, name: realName }); } }} + > {image.url ? {realName} : 1:1} + {canDelete && ( + + )}
); })} diff --git a/core/frontend/src/routes/team.tsx b/core/frontend/src/routes/team.tsx index 4d93b09..27daa99 100644 --- a/core/frontend/src/routes/team.tsx +++ b/core/frontend/src/routes/team.tsx @@ -548,6 +548,7 @@ export function TeamPage({ team, user, billing, navigate, onCreateMember: onCrea icon={} close={() => setModal("")} footer={} + dismissable={false} >