Compare commits
35 Commits
ad4bca31b1
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 3b72455062 | |||
| 526ad490f7 | |||
| 4993f6eeec | |||
| 79f220dbbf | |||
| c1c7cb3cc7 | |||
| b67f2e7601 | |||
| f056547160 | |||
| de3eb1d06a | |||
| f929be30ed | |||
| a2875738ce | |||
| 85adcdceef | |||
| 66b761314b | |||
| ab99e3bf2f | |||
| e3b48e2614 | |||
| 9a9c7eb86d | |||
| 5b316a2399 | |||
| 3f1954b38d | |||
| 96d335db8a | |||
| 307537a7ce | |||
| 48262d6233 | |||
| 062c8b3445 | |||
| 0b2d6b901f | |||
| e1fdbe5f9b | |||
| f51dfb17e1 | |||
| 76ae9ab0ac | |||
| 98db427ac5 | |||
| 573cbacbd3 | |||
| 38b513aebf | |||
| 4d5f487a80 | |||
| 4f6e32fb10 | |||
| 1f97167023 | |||
| 9ae5e1f493 | |||
| e88edbe165 | |||
| 863f1f075e | |||
| aa133d0f5c |
@@ -0,0 +1,43 @@
|
|||||||
|
# 自动检测文本文件并统一换行符
|
||||||
|
* text=auto eol=lf
|
||||||
|
|
||||||
|
# 源码强制使用 LF(跨平台一致)
|
||||||
|
*.ts text eol=lf
|
||||||
|
*.tsx text eol=lf
|
||||||
|
*.js text eol=lf
|
||||||
|
*.jsx text eol=lf
|
||||||
|
*.mjs text eol=lf
|
||||||
|
*.cjs text eol=lf
|
||||||
|
*.json text eol=lf
|
||||||
|
*.css text eol=lf
|
||||||
|
*.html text eol=lf
|
||||||
|
*.md text eol=lf
|
||||||
|
*.svg text eol=lf
|
||||||
|
|
||||||
|
# 配置类(统一 LF)
|
||||||
|
*.yml text eol=lf
|
||||||
|
*.yaml text eol=lf
|
||||||
|
*.toml text eol=lf
|
||||||
|
*.conf text eol=lf
|
||||||
|
|
||||||
|
# Windows 专用脚本保持 CRLF
|
||||||
|
*.bat text eol=crlf
|
||||||
|
*.cmd text eol=crlf
|
||||||
|
|
||||||
|
# 二进制文件,不做换行符转换
|
||||||
|
*.png binary
|
||||||
|
*.jpg binary
|
||||||
|
*.jpeg binary
|
||||||
|
*.gif binary
|
||||||
|
*.webp binary
|
||||||
|
*.ico binary
|
||||||
|
*.woff binary
|
||||||
|
*.woff2 binary
|
||||||
|
*.ttf binary
|
||||||
|
*.eot binary
|
||||||
|
*.otf binary
|
||||||
|
*.mp4 binary
|
||||||
|
*.mp3 binary
|
||||||
|
*.pdf binary
|
||||||
|
*.zip binary
|
||||||
|
*.gz binary
|
||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="zh-CN">
|
<html lang="zh-CN" data-theme="dark" data-ui-theme="dark-green" style="color-scheme: dark">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
|||||||
+21
-40
@@ -1,4 +1,4 @@
|
|||||||
import { Suspense, useCallback, useEffect, useMemo, useState } from "react";
|
import { lazy, Suspense, useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import {
|
import {
|
||||||
BugOutlined,
|
BugOutlined,
|
||||||
CheckCircleFilled,
|
CheckCircleFilled,
|
||||||
@@ -20,9 +20,7 @@ import {
|
|||||||
import ErrorBoundary from "./components/ErrorBoundary";
|
import ErrorBoundary from "./components/ErrorBoundary";
|
||||||
import ToastContainer from "./components/toast/ToastContainer";
|
import ToastContainer from "./components/toast/ToastContainer";
|
||||||
import { toast } from "./components/toast/toastStore";
|
import { toast } from "./components/toast/toastStore";
|
||||||
import EcommercePage from "./features/ecommerce/EcommercePage";
|
|
||||||
import { flushPendingGenerationRecords } from "./api/generationRecordClient";
|
import { flushPendingGenerationRecords } from "./api/generationRecordClient";
|
||||||
import { ossAssets } from "./data/ossAssets";
|
|
||||||
import { keyServerClient } from "./api/keyServerClient";
|
import { keyServerClient } from "./api/keyServerClient";
|
||||||
import { setUserMaxConcurrency } from "./api/generationConcurrency";
|
import { setUserMaxConcurrency } from "./api/generationConcurrency";
|
||||||
import {
|
import {
|
||||||
@@ -38,6 +36,8 @@ import { useAppStore, useSessionStore } from "./stores";
|
|||||||
import type { WebUserSession } from "./types";
|
import type { WebUserSession } from "./types";
|
||||||
import "./styles/ecommerce-standalone.css";
|
import "./styles/ecommerce-standalone.css";
|
||||||
|
|
||||||
|
const EcommercePage = lazy(() => import("./features/ecommerce/EcommercePage"));
|
||||||
|
|
||||||
type AuthMode = "login" | "register";
|
type AuthMode = "login" | "register";
|
||||||
type AuthMethod = "account" | "email" | "phone";
|
type AuthMethod = "account" | "email" | "phone";
|
||||||
|
|
||||||
@@ -51,17 +51,6 @@ interface LocalProfilePageProps {
|
|||||||
onLogout: () => void;
|
onLogout: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const profileWorks = [
|
|
||||||
{ title: "主图套图生成", desc: "电商主图与场景图自动生成", image: ossAssets.ecommerce.templateCases[0], type: "图像", time: "6/9 18:13" },
|
|
||||||
{ title: "A+详情页设计", desc: "产品卖点与长图详情版式", image: ossAssets.ecommerce.templateCases[1], type: "图像", time: "6/9 10:11" },
|
|
||||||
{ title: "短视频广告", desc: "产品展示短视频脚本与画面", image: ossAssets.ecommerce.productSet.hosting, type: "视频", time: "6/9 10:05" },
|
|
||||||
{ title: "模特图生成", desc: "服饰商品真人上身展示", image: ossAssets.ecommerce.tryOn.tryA, type: "图像", time: "6/9 10:03" },
|
|
||||||
{ title: "商品场景图", desc: "按平台比例输出营销素材", image: ossAssets.ecommerce.detail.gridA, type: "图像", time: "6/9 10:01" },
|
|
||||||
{ title: "高度复刻", desc: "参考图结构复刻与商品替换", image: ossAssets.ecommerce.detail.gridB, type: "图像", time: "6/9 09:39" },
|
|
||||||
{ title: "详情模块", desc: "功能卖点、参数和包装模块", image: ossAssets.ecommerce.detail.gridC, type: "图像", time: "6/8 21:20" },
|
|
||||||
{ title: "平台素材", desc: "淘宝/天猫投放图批量生成", image: ossAssets.ecommerce.detail.gridD, type: "图像", time: "6/8 18:26" },
|
|
||||||
];
|
|
||||||
|
|
||||||
function LocalAvatar({ session, size = "md" }: { session: WebUserSession; size?: "sm" | "md" | "lg" }) {
|
function LocalAvatar({ session, size = "md" }: { session: WebUserSession; size?: "sm" | "md" | "lg" }) {
|
||||||
const displayName = session.user.displayName || session.user.username || "用户";
|
const displayName = session.user.displayName || session.user.username || "用户";
|
||||||
const label = displayName.trim().slice(0, 1).toUpperCase() || "用";
|
const label = displayName.trim().slice(0, 1).toUpperCase() || "用";
|
||||||
@@ -75,9 +64,9 @@ function LocalAvatar({ session, size = "md" }: { session: WebUserSession; size?:
|
|||||||
|
|
||||||
function LocalProfilePage({ session, balance, imageCount, videoCount, onBack, onBugFeedback, onLogout }: LocalProfilePageProps) {
|
function LocalProfilePage({ session, balance, imageCount, videoCount, onBack, onBugFeedback, onLogout }: LocalProfilePageProps) {
|
||||||
const displayName = session.user.displayName || session.user.username || "用户";
|
const displayName = session.user.displayName || session.user.username || "用户";
|
||||||
const workCount = Math.max(imageCount + videoCount, profileWorks.length);
|
const workCount = Math.max(imageCount + videoCount, 0);
|
||||||
const projectCount = Math.max(1, Math.round(workCount / 18));
|
const projectCount = 0;
|
||||||
const assetCount = Math.max(1, Math.round(workCount / 20));
|
const assetCount = 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="local-profile-page">
|
<section className="local-profile-page">
|
||||||
@@ -142,22 +131,15 @@ function LocalProfilePage({ session, balance, imageCount, videoCount, onBack, on
|
|||||||
<header>
|
<header>
|
||||||
<div>
|
<div>
|
||||||
<strong>代表作</strong>
|
<strong>代表作</strong>
|
||||||
<span>最近完成的高质量生成内容</span>
|
<span>后续将展示接口返回的真实作品</span>
|
||||||
</div>
|
</div>
|
||||||
<em>{workCount} 项</em>
|
<em>{workCount} 项</em>
|
||||||
</header>
|
</header>
|
||||||
<div className="local-profile-work-grid">
|
<div className="local-profile-work-grid local-profile-work-grid--empty">
|
||||||
{profileWorks.map((work) => (
|
<div className="local-profile-empty">
|
||||||
<article key={`${work.title}-${work.time}`} className="local-profile-work-card">
|
<strong>暂无代表作数据</strong>
|
||||||
<img src={work.image} alt="" />
|
<span>作品接口接入后,这里会显示你的真实生成内容。</span>
|
||||||
<div>
|
|
||||||
<span>{work.type}</span>
|
|
||||||
<strong>{work.title}</strong>
|
|
||||||
<p>{work.desc}</p>
|
|
||||||
<em>已完成 · {work.time}</em>
|
|
||||||
</div>
|
</div>
|
||||||
</article>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
@@ -184,7 +166,6 @@ function App() {
|
|||||||
const [sessionNotice, setSessionNotice] = useState<string | null>(null);
|
const [sessionNotice, setSessionNotice] = useState<string | null>(null);
|
||||||
const [profileMenuOpen, setProfileMenuOpen] = useState(false);
|
const [profileMenuOpen, setProfileMenuOpen] = useState(false);
|
||||||
const [currentPage, setCurrentPage] = useState<"workspace" | "profile">("workspace");
|
const [currentPage, setCurrentPage] = useState<"workspace" | "profile">("workspace");
|
||||||
const [workspaceKey, setWorkspaceKey] = useState(0);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void loadDarkGreenTheme();
|
void loadDarkGreenTheme();
|
||||||
@@ -339,7 +320,7 @@ function App() {
|
|||||||
const balance = Math.max(usage.balanceCents, 0) / 100;
|
const balance = Math.max(usage.balanceCents, 0) / 100;
|
||||||
const displayName = session?.user.displayName || session?.user.username || "用户";
|
const displayName = session?.user.displayName || session?.user.username || "用户";
|
||||||
const actualWorkCount = Math.max(usage.imageUsed + usage.videoUsed, 0);
|
const actualWorkCount = Math.max(usage.imageUsed + usage.videoUsed, 0);
|
||||||
const shownWorkCount = Math.max(actualWorkCount, profileWorks.length);
|
const shownWorkCount = actualWorkCount;
|
||||||
|
|
||||||
const avatarMenuStats = useMemo(
|
const avatarMenuStats = useMemo(
|
||||||
() => [
|
() => [
|
||||||
@@ -360,7 +341,6 @@ function App() {
|
|||||||
const handleOpenWorkspace = () => {
|
const handleOpenWorkspace = () => {
|
||||||
setProfileMenuOpen(false);
|
setProfileMenuOpen(false);
|
||||||
setCurrentPage("workspace");
|
setCurrentPage("workspace");
|
||||||
setWorkspaceKey((k) => k + 1);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleBugFeedback = () => {
|
const handleBugFeedback = () => {
|
||||||
@@ -447,7 +427,8 @@ function App() {
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main className="ecommerce-standalone__content">
|
<main className="ecommerce-standalone__content">
|
||||||
{currentPage === "profile" && session ? (
|
{session ? (
|
||||||
|
<div className="ecommerce-standalone__page" hidden={currentPage !== "profile"}>
|
||||||
<LocalProfilePage
|
<LocalProfilePage
|
||||||
session={session}
|
session={session}
|
||||||
balance={balance}
|
balance={balance}
|
||||||
@@ -457,7 +438,11 @@ function App() {
|
|||||||
onBugFeedback={handleBugFeedback}
|
onBugFeedback={handleBugFeedback}
|
||||||
onLogout={handleLogout}
|
onLogout={handleLogout}
|
||||||
/>
|
/>
|
||||||
) : (
|
</div>
|
||||||
|
) : null}
|
||||||
|
{/* 工作台常驻挂载,仅用 hidden 切换。切到个人中心时不卸载,
|
||||||
|
生成任务、进度动画、已上传图片等本地状态全部保留,切回即继续。 */}
|
||||||
|
<div className="ecommerce-standalone__page" hidden={Boolean(session) && currentPage === "profile"}>
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<Suspense
|
<Suspense
|
||||||
fallback={
|
fallback={
|
||||||
@@ -468,7 +453,6 @@ function App() {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<EcommercePage
|
<EcommercePage
|
||||||
key={workspaceKey}
|
|
||||||
projects={[]}
|
projects={[]}
|
||||||
isAuthenticated={Boolean(session)}
|
isAuthenticated={Boolean(session)}
|
||||||
onStartCreate={() => undefined}
|
onStartCreate={() => undefined}
|
||||||
@@ -482,7 +466,7 @@ function App() {
|
|||||||
/>
|
/>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
)}
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
{authOpen ? (
|
{authOpen ? (
|
||||||
@@ -503,10 +487,7 @@ function App() {
|
|||||||
<CloseOutlined />
|
<CloseOutlined />
|
||||||
</button>
|
</button>
|
||||||
<span className="ecommerce-auth-modal__logo" aria-hidden="true">
|
<span className="ecommerce-auth-modal__logo" aria-hidden="true">
|
||||||
<i />
|
<img src="https://stringtest.oss-cn-hangzhou.aliyuncs.com/logo.png" alt="" />
|
||||||
<i />
|
|
||||||
<i />
|
|
||||||
<i />
|
|
||||||
</span>
|
</span>
|
||||||
<h2 id="ecommerce-auth-title">{authMode === "login" ? "欢迎回来" : "创建账号"}</h2>
|
<h2 id="ecommerce-auth-title">{authMode === "login" ? "欢迎回来" : "创建账号"}</h2>
|
||||||
<p className="ecommerce-auth-modal__subtitle">{authMode === "login" ? "登录后继续你的 AI 创作之旅" : "注册即可免费体验全部功能"}</p>
|
<p className="ecommerce-auth-modal__subtitle">{authMode === "login" ? "登录后继续你的 AI 创作之旅" : "注册即可免费体验全部功能"}</p>
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./aiGenerationClient.ts";
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./apiErrorUtils.ts";
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./generationRecordClient.ts";
|
||||||
@@ -38,6 +38,45 @@ export interface SaveGenerationRecordResult {
|
|||||||
id: string;
|
id: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 同一 clientRecordId 的保存去重:套图主流程、backgroundTaskRunner、useGenerationTasks
|
||||||
|
// 三处都可能对同一条终态任务调用 saveGenerationRecord,SSE 重复推送 completed 时
|
||||||
|
// 单个 poller 内也会重复触发。这里做客户端幂等:in-flight 合流 + 成功后短期拦截,
|
||||||
|
// 避免后端在缺少去重时插入重复记录。
|
||||||
|
const inFlightSaves = new Map<string, Promise<SaveGenerationRecordResult>>();
|
||||||
|
const recentlySavedRecords = new Map<string, { savedAt: number; signature: string }>();
|
||||||
|
const SAVE_DEDUPE_WINDOW_MS = 60_000;
|
||||||
|
|
||||||
|
function pruneRecentlySaved(now: number): void {
|
||||||
|
for (const [id, record] of recentlySavedRecords) {
|
||||||
|
if (now - record.savedAt > SAVE_DEDUPE_WINDOW_MS) recentlySavedRecords.delete(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function stableJsonStringify(value: unknown): string {
|
||||||
|
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
||||||
|
if (Array.isArray(value)) return `[${value.map(stableJsonStringify).join(",")}]`;
|
||||||
|
const entries = Object.entries(value as Record<string, unknown>)
|
||||||
|
.filter(([, entryValue]) => entryValue !== undefined)
|
||||||
|
.sort(([a], [b]) => a.localeCompare(b));
|
||||||
|
return `{${entries.map(([key, entryValue]) => `${JSON.stringify(key)}:${stableJsonStringify(entryValue)}`).join(",")}}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildSaveSignature(input: SaveGenerationRecordInput): string {
|
||||||
|
return stableJsonStringify({
|
||||||
|
tool: input.tool,
|
||||||
|
mode: input.mode,
|
||||||
|
title: input.title,
|
||||||
|
status: input.status,
|
||||||
|
prompt: input.prompt,
|
||||||
|
taskIds: input.taskIds,
|
||||||
|
assets: input.assets,
|
||||||
|
config: input.config,
|
||||||
|
result: input.result,
|
||||||
|
metadata: input.metadata,
|
||||||
|
createdAt: input.createdAt,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function readPendingRecords(): SaveGenerationRecordInput[] {
|
function readPendingRecords(): SaveGenerationRecordInput[] {
|
||||||
try {
|
try {
|
||||||
const raw = window.localStorage.getItem(PENDING_RECORDS_KEY);
|
const raw = window.localStorage.getItem(PENDING_RECORDS_KEY);
|
||||||
@@ -60,6 +99,39 @@ function writePendingRecord(input: SaveGenerationRecordInput): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function saveGenerationRecord(input: SaveGenerationRecordInput): Promise<SaveGenerationRecordResult> {
|
export async function saveGenerationRecord(input: SaveGenerationRecordInput): Promise<SaveGenerationRecordResult> {
|
||||||
|
const now = Date.now();
|
||||||
|
pruneRecentlySaved(now);
|
||||||
|
|
||||||
|
const recordId = input.clientRecordId;
|
||||||
|
const signature = buildSaveSignature(input);
|
||||||
|
if (recordId) {
|
||||||
|
const saveKey = `${recordId}:${signature}`;
|
||||||
|
const inFlight = inFlightSaves.get(saveKey);
|
||||||
|
if (inFlight) return inFlight;
|
||||||
|
const savedRecord = recentlySavedRecords.get(recordId);
|
||||||
|
if (savedRecord && savedRecord.signature === signature && now - savedRecord.savedAt <= SAVE_DEDUPE_WINDOW_MS) {
|
||||||
|
// 相同 clientRecordId 且 payload 完全一致时才拦截;同一记录的多轮更新需要继续保存。
|
||||||
|
return { source: "server", id: recordId };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const promise = saveGenerationRecordInternal(input);
|
||||||
|
if (recordId) {
|
||||||
|
const saveKey = `${recordId}:${signature}`;
|
||||||
|
inFlightSaves.set(saveKey, promise);
|
||||||
|
void promise
|
||||||
|
.then((result) => {
|
||||||
|
if (result.source === "server") recentlySavedRecords.set(recordId, { savedAt: Date.now(), signature });
|
||||||
|
})
|
||||||
|
.catch(() => undefined)
|
||||||
|
.finally(() => {
|
||||||
|
inFlightSaves.delete(saveKey);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveGenerationRecordInternal(input: SaveGenerationRecordInput): Promise<SaveGenerationRecordResult> {
|
||||||
try {
|
try {
|
||||||
const response = await serverRequest<{ id?: string | number }>("ai/generation-records", {
|
const response = await serverRequest<{ id?: string | number }>("ai/generation-records", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./serverConnection.ts";
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./taskSubscription.ts";
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./webGenerationGateway.ts";
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./toastStore.ts";
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./ossAssets.ts";
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./workflows.ts";
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export { default } from "./EcommercePage.tsx";
|
||||||
|
export * from "./EcommercePage.tsx";
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,10 +1,11 @@
|
|||||||
import { useSmoothedProgress } from "../../hooks/useSmoothedProgress";
|
import { useSmoothedProgress } from "../../hooks/useSmoothedProgress";
|
||||||
import type { ReactNode } from "react";
|
|
||||||
|
|
||||||
interface EcommerceProgressBarProps {
|
interface EcommerceProgressBarProps {
|
||||||
status: "idle" | "generating" | "done" | "failed" | string;
|
status: "idle" | "generating" | "done" | "failed" | string;
|
||||||
label?: string;
|
label?: string;
|
||||||
onCancel?: () => void;
|
onCancel?: () => void;
|
||||||
|
/** 0-100 真实进度。传入时进度条按真实值推进;省略时按状态做平滑蠕动。 */
|
||||||
|
progress?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapStatus(status: string): "running" | "completed" | "failed" {
|
function mapStatus(status: string): "running" | "completed" | "failed" {
|
||||||
@@ -14,9 +15,13 @@ function mapStatus(status: string): "running" | "completed" | "failed" {
|
|||||||
return "running";
|
return "running";
|
||||||
}
|
}
|
||||||
|
|
||||||
export function EcommerceProgressBar({ status, label, onCancel }: EcommerceProgressBarProps) {
|
export function EcommerceProgressBar({ status, label, onCancel, progress }: EcommerceProgressBarProps) {
|
||||||
const progress = mapStatus(status) === "running" ? 50 : 100;
|
const mapped = mapStatus(status);
|
||||||
const smoothed = useSmoothedProgress(progress, 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;
|
if (status === "idle") return null;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./ecommerceGenerationPersistence.ts";
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./ecommerceImageValidation.ts";
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./ecommerceTemplates.ts";
|
||||||
@@ -381,108 +381,6 @@ export default function EcommerceClonePanel({
|
|||||||
</section>
|
</section>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{cloneOutput === "hot" ? (
|
|
||||||
<section className="clone-ai-replicate-panel" aria-label="爆款图复刻设置">
|
|
||||||
<div className="clone-ai-dynamic-head">
|
|
||||||
<strong>爆款图参考设置</strong>
|
|
||||||
<span>随生成模式切换</span>
|
|
||||||
</div>
|
|
||||||
<div className="clone-ai-replicate-section">
|
|
||||||
<span className="clone-ai-replicate-title">参考内容</span>
|
|
||||||
<div className="clone-ai-replicate-tabs" role="tablist" aria-label="参考内容来源">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={cloneReferenceMode === "upload" ? "is-active" : ""}
|
|
||||||
aria-selected={cloneReferenceMode === "upload"}
|
|
||||||
onClick={() => setCloneReferenceMode("upload")}
|
|
||||||
>
|
|
||||||
上传参考图
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={cloneReferenceMode === "link" ? "is-active" : ""}
|
|
||||||
aria-selected={cloneReferenceMode === "link"}
|
|
||||||
onClick={() => setCloneReferenceMode("link")}
|
|
||||||
>
|
|
||||||
导入链接
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{cloneReferenceMode === "upload" ? (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={`clone-ai-replicate-upload${isCloneReferenceDragging ? " is-dragging" : ""}${cloneReferenceImages.length ? " has-files" : ""}`}
|
|
||||||
onClick={() => cloneReferenceInputRef.current?.click()}
|
|
||||||
onDragOver={handleCloneReferenceDragOver}
|
|
||||||
onDragLeave={handleCloneReferenceDragLeave}
|
|
||||||
onDrop={handleCloneReferenceDrop}
|
|
||||||
>
|
|
||||||
{cloneReferenceImages.length ? (
|
|
||||||
<>
|
|
||||||
<div className="clone-ai-replicate-files">
|
|
||||||
{cloneReferenceImages.map((item) => (
|
|
||||||
<figure
|
|
||||||
key={item.id}
|
|
||||||
className="clone-ai-replicate-file"
|
|
||||||
onMouseEnter={(e) => handleFileMouseEnter(item.src, e)}
|
|
||||||
onMouseLeave={handleFileMouseLeave}
|
|
||||||
>
|
|
||||||
<img src={item.src} alt="" />
|
|
||||||
</figure>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<span className="clone-ai-replicate-add-more">
|
|
||||||
<CloudUploadOutlined />
|
|
||||||
点击继续上传文件
|
|
||||||
</span>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<span>
|
|
||||||
<CloudUploadOutlined />
|
|
||||||
<span className="clone-ai-replicate-upload-text">拖拽或点击上传参考图</span>
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
<em>{cloneReferenceImages.length ? `已选 ${cloneReferenceImages.length}/${maxCloneReferenceImages}` : `最多 ${maxCloneReferenceImages} 张`}</em>
|
|
||||||
{isCloneReferenceDragging ? (
|
|
||||||
<div className="clone-ai-replicate-upload-overlay">
|
|
||||||
<CloudUploadOutlined />
|
|
||||||
<span>释放文件以上传</span>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</button>
|
|
||||||
) : (
|
|
||||||
<label className="clone-ai-replicate-link">
|
|
||||||
<input placeholder="粘贴商品图或详情页链接" />
|
|
||||||
</label>
|
|
||||||
)}
|
|
||||||
<input
|
|
||||||
ref={cloneReferenceInputRef}
|
|
||||||
type="file"
|
|
||||||
accept="image/jpeg,image/png,image/webp"
|
|
||||||
multiple
|
|
||||||
onChange={handleCloneReferenceUpload}
|
|
||||||
aria-label="上传参考图片"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="clone-ai-replicate-section">
|
|
||||||
<span className="clone-ai-replicate-title">复刻程度</span>
|
|
||||||
<div className="clone-ai-replicate-levels" role="toolbar" aria-label="复刻程度">
|
|
||||||
{cloneReplicateLevelOptions.map((option) => (
|
|
||||||
<button
|
|
||||||
key={option.key}
|
|
||||||
type="button"
|
|
||||||
className={cloneReplicateLevel === option.key ? "is-active" : ""}
|
|
||||||
aria-pressed={cloneReplicateLevel === option.key}
|
|
||||||
onClick={() => setCloneReplicateLevel(option.key)}
|
|
||||||
>
|
|
||||||
<strong>{option.title}</strong>
|
|
||||||
<span>{option.desc}</span>
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{cloneOutput === "set" ? (
|
{cloneOutput === "set" ? (
|
||||||
<section className="clone-ai-count-panel" aria-label="套图图片数量">
|
<section className="clone-ai-count-panel" aria-label="套图图片数量">
|
||||||
<div className="clone-ai-dynamic-head">
|
<div className="clone-ai-dynamic-head">
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./workbenchDownload.ts";
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./useGenerationTasks.ts";
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./useTypewriter.ts";
|
||||||
@@ -2,15 +2,6 @@ import React from "react";
|
|||||||
import ReactDOM from "react-dom/client";
|
import ReactDOM from "react-dom/client";
|
||||||
import "./styles/index.css";
|
import "./styles/index.css";
|
||||||
import App from "./App";
|
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");
|
const root = document.getElementById("root");
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./backgroundTaskRunner.ts";
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./index.ts";
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./useAppStore.ts";
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./useGenerationStore.ts";
|
||||||
@@ -24,17 +24,33 @@ interface PersistedQueueSnapshot {
|
|||||||
savedAt: number;
|
savedAt: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const STORAGE_KEY = "omniai:generation-queue";
|
const STORAGE_KEY_PREFIX = "omniai:generation-queue";
|
||||||
const MAX_ITEMS = 80;
|
const MAX_ITEMS = 80;
|
||||||
const STALE_MS = 2 * 60 * 60 * 1000; // 2 hours
|
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[] {
|
function loadPersistedQueue(): GenerationQueueItem[] {
|
||||||
try {
|
try {
|
||||||
const raw = localStorage.getItem(STORAGE_KEY);
|
const raw = localStorage.getItem(getStorageKey());
|
||||||
if (!raw) return [];
|
if (!raw) return [];
|
||||||
const snapshot = JSON.parse(raw) as PersistedQueueSnapshot;
|
const snapshot = JSON.parse(raw) as PersistedQueueSnapshot;
|
||||||
if (Date.now() - (snapshot.savedAt || 0) > STALE_MS) {
|
if (Date.now() - (snapshot.savedAt || 0) > STALE_MS) {
|
||||||
localStorage.removeItem(STORAGE_KEY);
|
localStorage.removeItem(getStorageKey());
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
return snapshot.items.filter(
|
return snapshot.items.filter(
|
||||||
@@ -48,7 +64,7 @@ function loadPersistedQueue(): GenerationQueueItem[] {
|
|||||||
function persistQueue(items: GenerationQueueItem[]): void {
|
function persistQueue(items: GenerationQueueItem[]): void {
|
||||||
try {
|
try {
|
||||||
const snapshot: PersistedQueueSnapshot = { version: 1, items, savedAt: Date.now() };
|
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 */ }
|
} catch { /* quota exceeded */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,17 +79,6 @@ interface GenerationStoreState {
|
|||||||
clearTerminal: () => void;
|
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();
|
const initialQueue = loadPersistedQueue();
|
||||||
|
|
||||||
export const useGenerationStore = create<GenerationStoreState>((set, get) => ({
|
export const useGenerationStore = create<GenerationStoreState>((set, get) => ({
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./useProjectStore.ts";
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./useSessionStore.ts";
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./useTaskStore.ts";
|
||||||
+4479
-16
File diff suppressed because it is too large
Load Diff
@@ -2930,12 +2930,68 @@
|
|||||||
height: auto;
|
height: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.product-clone-page[data-tool="clone"] .clone-ai-canvas-node .clone-ai-main-result {
|
||||||
|
width: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-clone-page[data-tool="clone"] .clone-ai-source-stack {
|
||||||
|
position: relative;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
.product-clone-page[data-tool="clone"] .clone-ai-result-grid {
|
.product-clone-page[data-tool="clone"] .clone-ai-result-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
gap: 14px;
|
gap: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.product-clone-page[data-tool="clone"] .clone-ai-result-stack {
|
||||||
|
position: relative;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-clone-page[data-tool="clone"] .clone-ai-result-stack > .clone-ai-node-label {
|
||||||
|
position: absolute;
|
||||||
|
top: -6px;
|
||||||
|
left: 50%;
|
||||||
|
z-index: 5;
|
||||||
|
transform: translate(-50%, -100%);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-clone-page[data-tool="clone"] .clone-ai-source-corner-action {
|
||||||
|
position: absolute;
|
||||||
|
top: -6px;
|
||||||
|
left: 50%;
|
||||||
|
z-index: 5;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 28px;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border: 1px solid rgba(0, 255, 136, 0.35);
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(21, 23, 28, 0.92);
|
||||||
|
box-shadow: 0 8px 22px rgba(0, 0, 0, 0.28);
|
||||||
|
color: #d8deed;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 900;
|
||||||
|
line-height: 1;
|
||||||
|
white-space: nowrap;
|
||||||
|
cursor: pointer;
|
||||||
|
transform: translate(-50%, -100%);
|
||||||
|
transition:
|
||||||
|
border-color 200ms ease,
|
||||||
|
transform 200ms ease,
|
||||||
|
box-shadow 200ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-clone-page[data-tool="clone"] .clone-ai-source-corner-action:hover {
|
||||||
|
border-color: #00ff88;
|
||||||
|
transform: translate(-50%, -106%);
|
||||||
|
box-shadow: 0 10px 26px rgba(0, 255, 136, 0.14), 0 8px 22px rgba(0, 0, 0, 0.28);
|
||||||
|
}
|
||||||
|
|
||||||
.product-clone-page[data-tool="clone"] .clone-ai-result-grid button {
|
.product-clone-page[data-tool="clone"] .clone-ai-result-grid button {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: auto;
|
height: auto;
|
||||||
@@ -7805,7 +7861,7 @@
|
|||||||
.product-set-preview-backdrop {
|
.product-set-preview-backdrop {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
z-index: 100;
|
z-index: 4000;
|
||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
background: rgb(17 24 39 / 58%);
|
background: rgb(17 24 39 / 58%);
|
||||||
@@ -12046,3 +12102,110 @@ html body #root .ecommerce-standalone.ecommerce-standalone .product-clone-page[d
|
|||||||
grid-row: auto !important;
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./types.ts";
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./enterpriseVideoPolicy.ts";
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./happyHorseRouting.ts";
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./pixverseRouting.ts";
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./resolveVideoModel.ts";
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./taskLifecycle.ts";
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./translateTaskError.ts";
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./viduRouting.ts";
|
||||||
+9
-5
@@ -2,8 +2,14 @@ import react from "@vitejs/plugin-react";
|
|||||||
import { compression } from "vite-plugin-compression2";
|
import { compression } from "vite-plugin-compression2";
|
||||||
import { defineConfig } from "vite";
|
import { defineConfig } from "vite";
|
||||||
|
|
||||||
export default defineConfig(() => {
|
export default defineConfig(({ command }) => {
|
||||||
const devApiTarget = process.env.OMNIAI_DEV_API_TARGET?.trim();
|
// 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
|
const apiProxy = devApiTarget
|
||||||
? {
|
? {
|
||||||
"/api": {
|
"/api": {
|
||||||
@@ -27,9 +33,7 @@ export default defineConfig(() => {
|
|||||||
port: 4174,
|
port: 4174,
|
||||||
host: "127.0.0.1",
|
host: "127.0.0.1",
|
||||||
},
|
},
|
||||||
esbuild: {
|
...(command === "build" ? { esbuild: { drop: ["console", "debugger"] } } : {}),
|
||||||
drop: ["console", "debugger"],
|
|
||||||
},
|
|
||||||
build: {
|
build: {
|
||||||
sourcemap: false,
|
sourcemap: false,
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
|
|||||||
Reference in New Issue
Block a user