setInspirationPreview({ mediaUrl: card.mediaUrl, mediaType: card.mediaType, prompt: buildInspirationPrompt(card.title, card.meta) })}>
{card.mediaType === "video" ? (
@@ -6135,13 +6273,19 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
正在翻译
AI 正在识别并翻译图片中的文字
- ) : translateStatus === "done" ? (
+ ) : translateStatus === "done" && translateResultUrl ? (
<>
-
+
>
+ ) : translateStatus === "failed" ? (
+
+
+ 翻译失败
+ 请重试或更换图片
+
) : (
diff --git a/src/features/ecommerce/EcommerceProgressBar.tsx b/src/features/ecommerce/EcommerceProgressBar.tsx
index f4fe063..8abf1cb 100644
--- a/src/features/ecommerce/EcommerceProgressBar.tsx
+++ b/src/features/ecommerce/EcommerceProgressBar.tsx
@@ -1,10 +1,11 @@
import { useSmoothedProgress } from "../../hooks/useSmoothedProgress";
-import type { ReactNode } from "react";
interface EcommerceProgressBarProps {
status: "idle" | "generating" | "done" | "failed" | string;
label?: string;
onCancel?: () => void;
+ /** 0-100 真实进度。传入时进度条按真实值推进;省略时按状态做平滑蠕动。 */
+ progress?: number;
}
function mapStatus(status: string): "running" | "completed" | "failed" {
@@ -14,9 +15,13 @@ function mapStatus(status: string): "running" | "completed" | "failed" {
return "running";
}
-export function EcommerceProgressBar({ status, label, onCancel }: EcommerceProgressBarProps) {
- const progress = mapStatus(status) === "running" ? 50 : 100;
- const smoothed = useSmoothedProgress(progress, mapStatus(status));
+export function EcommerceProgressBar({ status, label, onCancel, progress }: EcommerceProgressBarProps) {
+ const mapped = mapStatus(status);
+ // running 时目标取「真实进度」与兜底值 88 的较大者:有真实进度则跟随推进,
+ // 后端不推中间进度时也由平滑器持续蠕动到高位,不再卡死在 75%。
+ const realProgress = typeof progress === "number" ? Math.max(0, Math.min(100, progress)) : 0;
+ const target = mapped === "running" ? Math.max(realProgress, 88) : 100;
+ const smoothed = useSmoothedProgress(target, mapped);
if (status === "idle") return null;
diff --git a/src/main.tsx b/src/main.tsx
index 805d641..ee3b0a3 100644
--- a/src/main.tsx
+++ b/src/main.tsx
@@ -2,15 +2,6 @@ import React from "react";
import ReactDOM from "react-dom/client";
import "./styles/index.css";
import App from "./App";
-import { reportError } from "./utils/errorReporting";
-
-window.addEventListener("unhandledrejection", (event) => {
- reportError(event.reason, "rejection");
-});
-
-window.addEventListener("error", (event) => {
- if (event.error) reportError(event.error, "unhandled");
-});
const root = document.getElementById("root");
diff --git a/src/stores/useGenerationStore.ts b/src/stores/useGenerationStore.ts
index 711aa7c..167814a 100644
--- a/src/stores/useGenerationStore.ts
+++ b/src/stores/useGenerationStore.ts
@@ -24,17 +24,33 @@ interface PersistedQueueSnapshot {
savedAt: number;
}
-const STORAGE_KEY = "omniai:generation-queue";
+const STORAGE_KEY_PREFIX = "omniai:generation-queue";
const MAX_ITEMS = 80;
const STALE_MS = 2 * 60 * 60 * 1000; // 2 hours
+function hashUserId(): string {
+ try {
+ const raw = localStorage.getItem("omniai-web-session");
+ if (!raw) return "anon";
+ const parsed = JSON.parse(raw) as { user?: { id?: number | string } };
+ return String(parsed?.user?.id || "anon");
+ } catch {
+ return "anon";
+ }
+}
+
+// 队列按用户分桶持久化:不同账号读写不同 key,避免登出再登他人账号时读到上一个用户的队列。
+function getStorageKey(): string {
+ return `${STORAGE_KEY_PREFIX}:${hashUserId()}`;
+}
+
function loadPersistedQueue(): GenerationQueueItem[] {
try {
- const raw = localStorage.getItem(STORAGE_KEY);
+ const raw = localStorage.getItem(getStorageKey());
if (!raw) return [];
const snapshot = JSON.parse(raw) as PersistedQueueSnapshot;
if (Date.now() - (snapshot.savedAt || 0) > STALE_MS) {
- localStorage.removeItem(STORAGE_KEY);
+ localStorage.removeItem(getStorageKey());
return [];
}
return snapshot.items.filter(
@@ -48,7 +64,7 @@ function loadPersistedQueue(): GenerationQueueItem[] {
function persistQueue(items: GenerationQueueItem[]): void {
try {
const snapshot: PersistedQueueSnapshot = { version: 1, items, savedAt: Date.now() };
- localStorage.setItem(STORAGE_KEY, JSON.stringify(snapshot));
+ localStorage.setItem(getStorageKey(), JSON.stringify(snapshot));
} catch { /* quota exceeded */ }
}
@@ -63,17 +79,6 @@ interface GenerationStoreState {
clearTerminal: () => void;
}
-function hashUserId(): string {
- try {
- const raw = localStorage.getItem("omniai-web-session");
- if (!raw) return "anon";
- const parsed = JSON.parse(raw) as { user?: { id?: number | string } };
- return String(parsed?.user?.id || "anon");
- } catch {
- return "anon";
- }
-}
-
const initialQueue = loadPersistedQueue();
export const useGenerationStore = create((set, get) => ({
diff --git a/src/styles/ecommerce-standalone.css b/src/styles/ecommerce-standalone.css
index f5cb420..744be23 100644
--- a/src/styles/ecommerce-standalone.css
+++ b/src/styles/ecommerce-standalone.css
@@ -69,6 +69,17 @@
padding-top: 64px;
}
+/* 工作台与个人中心常驻同层,用 hidden 切换以保活生成任务状态。
+ wrapper 需要撑满内容区,让内部 .product-clone-page/.local-profile-page 的 height:100% 生效。 */
+.ecommerce-standalone__page {
+ height: 100%;
+ min-height: 0;
+}
+
+.ecommerce-standalone__page[hidden] {
+ display: none !important;
+}
+
.ecommerce-standalone__content > .error-boundary,
.ecommerce-standalone__content .product-clone-page {
height: 100%;
@@ -1346,6 +1357,34 @@
font-weight: 500;
}
+.local-profile-work-grid--empty {
+ display: block;
+}
+
+.local-profile-empty {
+ display: grid;
+ min-height: 220px;
+ place-items: center;
+ gap: 8px;
+ padding: 36px 20px;
+ border: 1px dashed rgba(30, 189, 219, 0.22);
+ border-radius: 18px;
+ color: #6c7d88;
+ text-align: center;
+ background: #f8fbfc;
+}
+
+.local-profile-empty strong {
+ color: #10202c;
+ font-size: 15px;
+}
+
+.local-profile-empty span {
+ max-width: 360px;
+ font-size: 13px;
+ line-height: 1.6;
+}
+
@media (max-width: 980px) {
.local-profile-page__body {
grid-template-columns: minmax(0, 1fr);
@@ -12354,6 +12393,40 @@ body .ecom-inspiration-preview__close {
display: none !important;
}
+/* 灵感预览:右下角"使用此提示词"动作条,避开视频底部控制条。 */
+body .ecom-inspiration-preview__actions {
+ position: absolute !important;
+ right: 16px !important;
+ bottom: 16px !important;
+ z-index: 2 !important;
+ display: flex !important;
+ gap: 10px !important;
+}
+
+body .ecom-inspiration-preview__use-prompt {
+ display: inline-flex !important;
+ align-items: center !important;
+ gap: 8px !important;
+ padding: 10px 20px !important;
+ border: 1px solid rgba(255, 255, 255, 0.28) !important;
+ border-radius: 999px !important;
+ background: rgba(16, 32, 44, 0.72) !important;
+ backdrop-filter: blur(8px) !important;
+ -webkit-backdrop-filter: blur(8px) !important;
+ color: #ffffff !important;
+ font-size: 14px !important;
+ font-weight: 600 !important;
+ cursor: pointer !important;
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.28) !important;
+ transition: background 160ms ease, transform 160ms ease, border-color 160ms ease !important;
+}
+
+body .ecom-inspiration-preview__use-prompt:hover {
+ border-color: rgba(30, 189, 219, 0.6) !important;
+ background: rgba(30, 189, 219, 0.92) !important;
+ transform: translateY(-1px) !important;
+}
+
@media (max-width: 760px) {
body .ecom-inspiration-preview {
padding: 14px !important;
@@ -13934,3 +14007,77 @@ html body #root .ecommerce-standalone.ecommerce-standalone .product-clone-page[d
min-height: 36px !important;
max-height: 36px !important;
}
+
+/* Composer menu anchors: place option popovers under the clicked control, not under the whole composer. */
+html body #root .ecommerce-standalone.ecommerce-standalone .product-clone-page[data-tool="clone"][data-tool="clone"] .clone-ai-input-wrapper.ecom-command-composer > .ecom-command-popover.ecom-command-popover {
+ position: absolute !important;
+ inset: var(--composer-popover-top, 48px) auto auto var(--composer-popover-left, 0px) !important;
+ right: auto !important;
+ bottom: auto !important;
+ margin: 0 !important;
+ transform: none !important;
+ translate: none !important;
+ z-index: 160 !important;
+}
+
+html body #root .ecommerce-standalone.ecommerce-standalone .product-clone-page[data-tool="clone"][data-tool="clone"] .clone-ai-input-wrapper.ecom-command-composer > .ecom-command-popover.ecom-command-popover--platform {
+ width: min(360px, calc(100% - var(--composer-popover-left, 0px))) !important;
+ max-width: min(360px, calc(100% - var(--composer-popover-left, 0px))) !important;
+}
+
+html body #root .ecommerce-standalone.ecommerce-standalone .product-clone-page[data-tool="clone"][data-tool="clone"] .clone-ai-input-wrapper.ecom-command-composer > .ecom-command-popover.ecom-command-popover--languages,
+html body #root .ecommerce-standalone.ecommerce-standalone .product-clone-page[data-tool="clone"][data-tool="clone"] .clone-ai-input-wrapper.ecom-command-composer > .ecom-command-popover.ecom-command-popover--ratio-picker {
+ width: min(420px, calc(100% - var(--composer-popover-left, 0px))) !important;
+ max-width: min(420px, calc(100% - var(--composer-popover-left, 0px))) !important;
+}
+
+html body #root .ecommerce-standalone.ecommerce-standalone .product-clone-page[data-tool="clone"][data-tool="clone"] .clone-ai-input-wrapper.ecom-command-composer > .ecom-command-popover.ecom-command-popover--settings {
+ width: min(520px, calc(100% - var(--composer-popover-left, 0px))) !important;
+ max-width: min(520px, calc(100% - var(--composer-popover-left, 0px))) !important;
+}
+
+/* Uploaded assets stay as compact attachments inside the composer hierarchy. */
+html body #root .ecommerce-standalone.ecommerce-standalone .product-clone-page[data-tool="clone"][data-tool="clone"] .clone-ai-input-wrapper.ecom-command-composer:has(.ecom-command-asset-popover) {
+ min-height: clamp(224px, 18vh, 250px) !important;
+}
+
+html body #root .ecommerce-standalone.ecommerce-standalone .product-clone-page[data-tool="clone"][data-tool="clone"] .ecom-command-asset-popover,
+html body #root .ecommerce-standalone.ecommerce-standalone .product-clone-page[data-tool="clone"][data-tool="clone"] .ecom-command-composer-wrap:not(.has-generated.is-compact) .clone-ai-input-wrapper.ecom-command-composer:has(.ecom-command-asset-popover) .ecom-command-asset-popover {
+ position: static !important;
+ grid-column: 1 !important;
+ display: inline-flex !important;
+ align-items: center !important;
+ justify-content: flex-start !important;
+ justify-self: start !important;
+ gap: 8px !important;
+ width: auto !important;
+ max-width: min(100%, 420px) !important;
+ min-height: 48px !important;
+ max-height: 52px !important;
+ padding: 0 !important;
+ overflow-x: auto !important;
+ overflow-y: visible !important;
+ border: 0 !important;
+ border-radius: 0 !important;
+ background: transparent !important;
+ box-shadow: none !important;
+ transform: none !important;
+}
+
+html body #root .ecommerce-standalone.ecommerce-standalone .product-clone-page[data-tool="clone"][data-tool="clone"] .ecom-command-asset-thumb,
+html body #root .ecommerce-standalone.ecommerce-standalone .product-clone-page[data-tool="clone"][data-tool="clone"] .ecom-command-composer-wrap:not(.has-generated.is-compact) .clone-ai-input-wrapper.ecom-command-composer:has(.ecom-command-asset-popover) .ecom-command-asset-thumb {
+ flex: 0 0 48px !important;
+ width: 48px !important;
+ height: 48px !important;
+ border-radius: 12px !important;
+}
+
+html body #root .ecommerce-standalone.ecommerce-standalone .product-clone-page[data-tool="clone"][data-tool="clone"] .ecom-command-asset-add,
+html body #root .ecommerce-standalone.ecommerce-standalone .product-clone-page[data-tool="clone"][data-tool="clone"] .ecom-command-composer-wrap:not(.has-generated.is-compact) .clone-ai-input-wrapper.ecom-command-composer:has(.ecom-command-asset-popover) .ecom-command-asset-add {
+ flex: 0 0 34px !important;
+ width: 34px !important;
+ height: 34px !important;
+ min-height: 34px !important;
+ margin: 0 !important;
+ font-size: 22px !important;
+}
diff --git a/src/styles/pages/ecommerce.css b/src/styles/pages/ecommerce.css
index 4ce9dee..8d71d84 100644
--- a/src/styles/pages/ecommerce.css
+++ b/src/styles/pages/ecommerce.css
@@ -12093,3 +12093,110 @@ html body #root .ecommerce-standalone.ecommerce-standalone .product-clone-page[d
grid-row: auto !important;
}
}
+
+/* Composer menu anchors: place option popovers under the clicked control, not under the whole composer. */
+html body #root .ecommerce-standalone.ecommerce-standalone .product-clone-page[data-tool="clone"][data-tool="clone"] .clone-ai-input-wrapper.ecom-command-composer > .ecom-command-popover.ecom-command-popover {
+ position: absolute !important;
+ inset: var(--composer-popover-top, 48px) auto auto var(--composer-popover-left, 0px) !important;
+ right: auto !important;
+ bottom: auto !important;
+ margin: 0 !important;
+ transform: none !important;
+ translate: none !important;
+ z-index: 160 !important;
+}
+
+html body #root .ecommerce-standalone.ecommerce-standalone .product-clone-page[data-tool="clone"][data-tool="clone"] .clone-ai-input-wrapper.ecom-command-composer > .ecom-command-popover.ecom-command-popover--platform {
+ grid-template-columns: repeat(2, minmax(0, 1fr)) !important;
+}
+
+/* 平台弹窗宽度仅桌面/平板固定;≤640px 由移动端断点的全宽规则接管。 */
+@media (min-width: 641px) {
+ html body #root .ecommerce-standalone.ecommerce-standalone .product-clone-page[data-tool="clone"][data-tool="clone"] .clone-ai-input-wrapper.ecom-command-composer > .ecom-command-popover.ecom-command-popover--platform {
+ width: min(460px, calc(100% - 24px)) !important;
+ max-width: min(460px, calc(100% - 24px)) !important;
+ }
+}
+
+/* 平台选项:logo + 名称横排,名称过长省略,避免在窄网格里溢出弹窗。 */
+html body #root .ecommerce-standalone.ecommerce-standalone .product-clone-page[data-tool="clone"][data-tool="clone"] .ecom-command-popover--platform button {
+ display: flex !important;
+ align-items: center !important;
+ justify-content: flex-start !important;
+ gap: 8px !important;
+ min-width: 0 !important;
+ text-align: left !important;
+}
+
+html body #root .ecommerce-standalone.ecommerce-standalone .product-clone-page[data-tool="clone"][data-tool="clone"] .ecom-command-popover--platform button .ecom-platform-name {
+ min-width: 0 !important;
+ overflow: hidden !important;
+ text-overflow: ellipsis !important;
+ white-space: nowrap !important;
+}
+
+@media (min-width: 641px) {
+ html body #root .ecommerce-standalone.ecommerce-standalone .product-clone-page[data-tool="clone"][data-tool="clone"] .clone-ai-input-wrapper.ecom-command-composer > .ecom-command-popover.ecom-command-popover--languages,
+ html body #root .ecommerce-standalone.ecommerce-standalone .product-clone-page[data-tool="clone"][data-tool="clone"] .clone-ai-input-wrapper.ecom-command-composer > .ecom-command-popover.ecom-command-popover--ratio-picker {
+ width: max-content !important;
+ min-width: 200px !important;
+ max-width: min(420px, calc(100% - var(--composer-popover-left, 0px))) !important;
+ }
+}
+
+/* 宽设置面板:固定宽度并靠右对齐 composer,避免从靠右的"设置"按钮左对齐展开时顶出右边缘被裁。
+ 仅桌面/平板生效;≤640px 由移动端断点的全宽规则接管。 */
+@media (min-width: 641px) {
+ html body #root .ecommerce-standalone.ecommerce-standalone .product-clone-page[data-tool="clone"][data-tool="clone"] .clone-ai-input-wrapper.ecom-command-composer > .ecom-command-popover.ecom-command-popover--settings {
+ width: min(520px, calc(100% - 24px)) !important;
+ max-width: min(520px, calc(100% - 24px)) !important;
+ left: auto !important;
+ inset: var(--composer-popover-top, 48px) 12px auto auto !important;
+ }
+}
+
+/* Uploaded assets stay as compact attachments inside the composer hierarchy. */
+html body #root .ecommerce-standalone.ecommerce-standalone .product-clone-page[data-tool="clone"][data-tool="clone"] .clone-ai-input-wrapper.ecom-command-composer:has(.ecom-command-asset-popover) {
+ min-height: 0 !important;
+}
+
+html body #root .ecommerce-standalone.ecommerce-standalone .product-clone-page[data-tool="clone"][data-tool="clone"] .ecom-command-asset-popover,
+html body #root .ecommerce-standalone.ecommerce-standalone .product-clone-page[data-tool="clone"][data-tool="clone"] .ecom-command-composer-wrap:not(.has-generated.is-compact) .clone-ai-input-wrapper.ecom-command-composer:has(.ecom-command-asset-popover) .ecom-command-asset-popover {
+ position: static !important;
+ grid-column: 1 !important;
+ display: flex !important;
+ flex-wrap: wrap !important;
+ align-items: center !important;
+ justify-content: flex-start !important;
+ justify-self: start !important;
+ gap: 10px !important;
+ width: auto !important;
+ max-width: 100% !important;
+ min-height: 0 !important;
+ max-height: none !important;
+ padding: 2px 2px 0 !important;
+ overflow: visible !important;
+ border: 0 !important;
+ border-radius: 0 !important;
+ background: transparent !important;
+ box-shadow: none !important;
+ transform: none !important;
+}
+
+html body #root .ecommerce-standalone.ecommerce-standalone .product-clone-page[data-tool="clone"][data-tool="clone"] .ecom-command-asset-thumb,
+html body #root .ecommerce-standalone.ecommerce-standalone .product-clone-page[data-tool="clone"][data-tool="clone"] .ecom-command-composer-wrap:not(.has-generated.is-compact) .clone-ai-input-wrapper.ecom-command-composer:has(.ecom-command-asset-popover) .ecom-command-asset-thumb {
+ flex: 0 0 64px !important;
+ width: 64px !important;
+ height: 64px !important;
+ border-radius: 14px !important;
+}
+
+html body #root .ecommerce-standalone.ecommerce-standalone .product-clone-page[data-tool="clone"][data-tool="clone"] .ecom-command-asset-add,
+html body #root .ecommerce-standalone.ecommerce-standalone .product-clone-page[data-tool="clone"][data-tool="clone"] .ecom-command-composer-wrap:not(.has-generated.is-compact) .clone-ai-input-wrapper.ecom-command-composer:has(.ecom-command-asset-popover) .ecom-command-asset-add {
+ flex: 0 0 44px !important;
+ width: 44px !important;
+ height: 64px !important;
+ min-height: 44px !important;
+ margin: 0 !important;
+ font-size: 24px !important;
+}
diff --git a/vite.config.ts b/vite.config.ts
index d963bac..1688ca7 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -2,8 +2,14 @@ import react from "@vitejs/plugin-react";
import { compression } from "vite-plugin-compression2";
import { defineConfig } from "vite";
-export default defineConfig(() => {
- const devApiTarget = process.env.OMNIAI_DEV_API_TARGET?.trim();
+export default defineConfig(({ command }) => {
+ // dev 模式下默认把 /api 代理到线上电商后端,本地 `npm run dev` 即可直接登录/生成。
+ // 想连本地或 SSH 隧道的后端时,用环境变量覆盖:
+ // $env:OMNIAI_DEV_API_TARGET="http://127.0.0.1:3601"; npm run dev
+ // 仅 dev 代理用途,不会打进生产构建产物。
+ const devApiTarget =
+ process.env.OMNIAI_DEV_API_TARGET?.trim() ||
+ (command === "serve" ? "https://omniai.com.cn" : "");
const apiProxy = devApiTarget
? {
"/api": {
@@ -27,9 +33,7 @@ export default defineConfig(() => {
port: 4174,
host: "127.0.0.1",
},
- esbuild: {
- drop: ["console", "debugger"],
- },
+ ...(command === "build" ? { esbuild: { drop: ["console", "debugger"] } } : {}),
build: {
sourcemap: false,
rollupOptions: {