event.stopPropagation()}>
++
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
+ ?
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