diff --git a/src-tauri/icons/icon.png b/src-tauri/icons/icon.png index 673d07b..5c17473 100644 Binary files a/src-tauri/icons/icon.png and b/src-tauri/icons/icon.png differ diff --git a/src-tauri/src/commands/generation.rs b/src-tauri/src/commands/generation.rs index efb4650..64c9680 100644 --- a/src-tauri/src/commands/generation.rs +++ b/src-tauri/src/commands/generation.rs @@ -54,7 +54,9 @@ pub async fn generate_image( let model = input .model .clone() + .filter(|model| !model.trim().is_empty()) .or_else(|| provider.image_model.clone()) + .filter(|model| !model.trim().is_empty()) .ok_or_else(|| AppError::Provider("image model is empty".to_string()))?; let task = repository::create_generation_task( diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 2388149..684e21a 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -13,23 +13,23 @@ "windows": [ { "title": "Image Draw AI", - "width": 1100, - "height": 760, - "minWidth": 900, - "minHeight": 620 + "width": 1280, + "height": 1160, + "minWidth": 1180, + "minHeight": 1100 } ], "security": { "csp": null, "assetProtocol": { "enable": true, - "scope": ["$APPDATA/**"] + "scope": ["$APPDATA/**", "$HOME/**", "$PICTURE/**", "$DOWNLOAD/**", "$DESKTOP/**", "$DOCUMENT/**"] } } }, "bundle": { "active": true, "targets": "all", - "icon": [] + "icon": ["icons/icon.png"] } } diff --git a/src/assets/logo.svg b/src/assets/logo.svg new file mode 100644 index 0000000..6776b6f --- /dev/null +++ b/src/assets/logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/main.tsx b/src/main.tsx index af11b83..245d9cd 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,6 +1,7 @@ import React, { useEffect, useState } from 'react'; import ReactDOM from 'react-dom/client'; import { convertFileSrc, invoke } from '@tauri-apps/api/core'; +import appLogo from './assets/logo.svg'; import './styles.css'; type ProviderConfig = { @@ -20,8 +21,8 @@ type ProviderForm = { kind: string; base_url: string; api_key: string; - text_model: string; - image_model: string; + text_model?: string | null; + image_model?: string | null; enabled: boolean; }; @@ -60,8 +61,8 @@ const defaultProviderForm: ProviderForm = { kind: 'openai-compatible', base_url: 'https://api.openai.com/v1', api_key: '', - text_model: 'gpt-5', - image_model: 'gpt-image-2', + text_model: null, + image_model: null, enabled: true, }; @@ -73,10 +74,15 @@ const initialGenerationSteps: GenerationStep[] = [ { label: '更新结果列表', status: 'pending' }, ]; +const imageModelOptions = ['gpt-image-1', 'gpt-image-1.5', 'gpt-image-2']; +const imageSizeOptions = ['1024x1024', '1024x1536', '1536x1024']; +const imageQualityOptions = ['auto', 'high', 'medium', 'low']; + function App() { const [providers, setProviders] = useState([]); const [providerForm, setProviderForm] = useState(defaultProviderForm); const [prompt, setPrompt] = useState('一只赛博朋克风格的橘猫坐在霓虹灯下'); + const [selectedImageModel, setSelectedImageModel] = useState('gpt-image-2'); const [imageSize, setImageSize] = useState('1024x1024'); const [imageQuality, setImageQuality] = useState('auto'); const [status, setStatus] = useState('准备就绪'); @@ -119,8 +125,8 @@ function App() { kind: current.kind, base_url: current.base_url, api_key: current.api_key ?? form.api_key, - text_model: current.text_model ?? defaultProviderForm.text_model, - image_model: current.image_model ?? defaultProviderForm.image_model, + text_model: current.text_model ?? null, + image_model: null, enabled: current.enabled, })); } @@ -130,7 +136,7 @@ function App() { setIsBusy(true); setStatus('正在保存配置...'); try { - await invoke('upsert_provider', { input: providerForm }); + await invoke('upsert_provider', { input: { ...providerForm, image_model: null } }); await refreshProviders(); setStatus('配置已保存'); } catch (error) { @@ -165,8 +171,8 @@ function App() { kind: provider.kind, base_url: provider.base_url, api_key: provider.api_key ?? form.api_key, - text_model: provider.text_model ?? defaultProviderForm.text_model, - image_model: provider.image_model ?? defaultProviderForm.image_model, + text_model: provider.text_model ?? null, + image_model: null, enabled: provider.enabled, })); setStatus('已切换模型配置'); @@ -178,7 +184,7 @@ function App() { setStatus('正在生成图片...'); try { startStep(0); - await invoke('upsert_provider', { input: providerForm }); + await invoke('upsert_provider', { input: { ...providerForm, image_model: null } }); startStep(1); await new Promise((resolve) => window.setTimeout(resolve, 120)); startStep(2); @@ -186,7 +192,7 @@ function App() { input: { provider_id: providerForm.id, prompt, - model: providerForm.image_model, + model: selectedImageModel, size: imageSize, quality: imageQuality, image_paths: materialPaths, @@ -260,7 +266,7 @@ function App() {
-
+ Image Draw AI

Image Draw AI

图片默认保存到应用数据文件夹

@@ -269,7 +275,7 @@ function App() {
当前模型 - {providerForm.image_model} + {selectedImageModel}
@@ -288,25 +294,30 @@ function App() {
-
- - {materialPaths.length > 0 && ( - - )} - {materialPaths.length > 0 ? `${materialPaths.length} 张素材` : '未导入素材'} -
-

支持 PNG / JPG / WEBP,多张素材会使用图像编辑模式

- - {materialPaths.length > 0 && ( -
- {materialPaths.map((path) => ( -
- 素材图片 - -
- ))} +
+
+ 参考图 + {materialPaths.length > 0 ? `${materialPaths.length} 张,图像编辑模式` : '可选,支持多张'}
- )} + {materialPaths.length > 0 && ( + + )} +
+ +
+ + {materialPaths.map((path, index) => ( +
+ 素材图片 + {index + 1} + +
+ ))} +
@@ -314,29 +325,31 @@ function App() { 生成参数 基础
-
- {['1024x1024', '1024x1536', '1536x1024'].map((size) => ( - - ))} -
-
- {['auto', 'high', 'medium', 'low'].map((quality) => ( - - ))} + +
+ +
@@ -344,22 +357,7 @@ function App() { {isBusy ? '正在生成...' : '开始生成'} -
- - -

{status}

+ {status !== '准备就绪' &&

{status}

}
@@ -393,6 +391,21 @@ function App() { ))}
)} + +
+ @@ -452,23 +465,6 @@ function App() { -
-
- 模型 - 默认模型 -
-
- - -
-
-
@@ -489,7 +485,6 @@ function App() {
diff --git a/src/styles.css b/src/styles.css index f4dc02b..237180f 100644 --- a/src/styles.css +++ b/src/styles.css @@ -1,6 +1,9 @@ :root { color: #172033; - background: #eef3fb; + background: + radial-gradient(circle at 8% 10%, rgba(255, 221, 77, 0.22), transparent 28%), + radial-gradient(circle at 92% 8%, rgba(96, 239, 154, 0.18), transparent 24%), + linear-gradient(135deg, #f7f9ff 0%, #eef4fb 46%, #f8fbf6 100%); font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "PingFang SC", "Microsoft YaHei", "Segoe UI", sans-serif; } @@ -10,9 +13,10 @@ body { margin: 0; + overflow: hidden; } -button, textarea, input { +button, textarea, input, select { font: inherit; } @@ -54,21 +58,28 @@ button.mini { } .app-shell { - min-height: 100vh; - padding: 22px; + display: grid; + gap: 18px; + grid-template-rows: auto minmax(0, 1fr); + height: 100vh; + min-width: 1120px; + overflow: hidden; + padding: 20px; } .topbar { align-items: center; - background: rgba(255, 255, 255, 0.82); - border: 1px solid rgba(203, 213, 225, 0.8); - border-radius: 24px; - box-shadow: 0 18px 50px rgba(15, 23, 42, 0.08); + backdrop-filter: blur(18px); + background: rgba(255, 255, 255, 0.76); + border: 1px solid rgba(255, 255, 255, 0.86); + border-radius: 28px; + box-shadow: 0 20px 60px rgba(30, 41, 59, 0.1), inset 0 1px 0 rgba(255, 255, 255, 0.72); display: flex; justify-content: space-between; - margin: 0 auto 18px; + margin: 0 auto; max-width: 1440px; padding: 16px 18px; + width: 100%; } .brand { @@ -78,15 +89,12 @@ button.mini { } .brand-mark { - align-items: center; - background: linear-gradient(135deg, #2563eb, #7c3aed); + background: linear-gradient(180deg, #ffffff, #f8fafc); border-radius: 18px; - color: #ffffff; - display: grid; - font-size: 24px; - font-weight: 900; + box-shadow: 0 12px 28px rgba(15, 23, 42, 0.12), inset 0 0 0 1px rgba(15, 23, 42, 0.08); height: 52px; - place-items: center; + object-fit: contain; + padding: 6px; width: 52px; } @@ -108,8 +116,8 @@ button.mini { } .current-provider { - background: #f8fafc; - border: 1px solid #e2e8f0; + background: rgba(248, 250, 252, 0.88); + border: 1px solid rgba(226, 232, 240, 0.96); border-radius: 16px; display: grid; gap: 4px; @@ -129,29 +137,56 @@ button.mini { } .workspace { + align-items: stretch; display: grid; gap: 18px; grid-template-columns: minmax(360px, 440px) minmax(0, 1fr); margin: 0 auto; max-width: 1440px; + height: 100%; + min-height: 0; + width: 100%; } .compose-card, .result-card, .settings-drawer { - background: rgba(255, 255, 255, 0.9); - border: 1px solid rgba(203, 213, 225, 0.9); - border-radius: 28px; - box-shadow: 0 24px 70px rgba(15, 23, 42, 0.1); + backdrop-filter: blur(18px); + background: rgba(255, 255, 255, 0.86); + border: 1px solid rgba(255, 255, 255, 0.9); + border-radius: 30px; + box-shadow: 0 28px 80px rgba(30, 41, 59, 0.12), inset 0 1px 0 rgba(255, 255, 255, 0.74); } .compose-card { - align-self: start; + display: flex; + flex-direction: column; + gap: 12px; + min-height: 0; + overflow: hidden; + padding: 16px; +} + +.compose-card > .section-heading, +.compose-card > .material-panel, +.compose-card > .params-card, +.compose-card > .generate-button, +.compose-card > .status { + flex: 0 0 auto; +} + +.prompt-field { display: grid; - gap: 18px; - padding: 20px; + grid-template-rows: auto minmax(0, 1fr); + flex: 1 1 auto; + gap: 8px; + min-height: 260px; + overflow: hidden; } .result-card { - min-height: calc(100vh - 132px); + display: flex; + flex-direction: column; + min-height: 0; + overflow: hidden; padding: 20px; } @@ -160,6 +195,7 @@ button.mini { display: flex; justify-content: space-between; gap: 12px; + min-height: 34px; } .heading-actions { @@ -180,25 +216,36 @@ button.mini { font-weight: 800; } -input, textarea { +input, textarea, select { + max-width: 100%; width: 100%; border: 1px solid #dbe4f0; border-radius: 18px; color: #172033; - background: #f8fafc; + background: rgba(248, 250, 252, 0.88); outline: none; padding: 13px 14px; } -input:focus, textarea:focus { +input:focus, textarea:focus, select:focus { background: #ffffff; border-color: #60a5fa; box-shadow: 0 0 0 4px rgba(96, 165, 250, 0.18); } +select { + appearance: none; + background-image: linear-gradient(45deg, transparent 50%, #64748b 50%), linear-gradient(135deg, #64748b 50%, transparent 50%); + background-position: calc(100% - 18px) 52%, calc(100% - 12px) 52%; + background-size: 6px 6px, 6px 6px; + background-repeat: no-repeat; +} + .prompt-field textarea { - min-height: 190px; - resize: vertical; + height: 100%; + min-height: 0; + max-width: 100%; + resize: none; } small { @@ -216,76 +263,154 @@ small { } .material-panel { - background: #f8fafc; - border: 1px dashed #cbd5e1; + background: rgba(248, 250, 252, 0.72); + border: 1px solid rgba(226, 232, 240, 0.92); border-radius: 22px; - padding: 14px; + display: grid; + gap: 12px; + grid-template-rows: auto 120px; + height: 200px; + overflow: hidden; + padding: 12px; } -.material-toolbar { +.material-header { align-items: center; display: flex; - flex-wrap: wrap; - gap: 10px; + justify-content: space-between; + gap: 12px; } -.material-toolbar span { +.material-header div { + display: grid; + gap: 3px; +} + +.material-header strong { + color: #0f172a; +} + +.material-header span { color: #64748b; + font-size: 12px; +} + +.reference-strip { + display: flex; + gap: 10px; + min-height: 120px; + overflow-x: auto; + overflow-y: hidden; + padding: 8px 2px; +} + +.add-reference-card, .reference-card { + border-radius: 18px; + flex: 0 0 104px; + height: 104px; +} + +.add-reference-card { + align-items: center; + background: linear-gradient(180deg, #ffffff, #f8fafc); + border: 1px dashed rgba(37, 99, 235, 0.42); + color: #2563eb; + display: grid; + gap: 2px; + justify-items: center; + padding: 12px; +} + +.add-reference-card:hover:not(:disabled) { + box-shadow: 0 14px 28px rgba(37, 99, 235, 0.14); +} + +.add-reference-card > span { + align-items: center; + background: #eff6ff; + border-radius: 999px; + display: grid; + font-size: 22px; + height: 34px; + place-items: center; + width: 34px; +} + +.add-reference-card strong { font-size: 13px; } -.drop-hint { - color: #94a3b8; - font-size: 12px; - margin: 10px 0 0; +.add-reference-card small { + font-size: 11px; } -.material-grid { - display: grid; - gap: 10px; - grid-template-columns: repeat(auto-fill, minmax(96px, 1fr)); - margin-top: 12px; -} - -.material-card { +.reference-card { background: #ffffff; border: 1px solid #e2e8f0; - border-radius: 18px; - display: grid; - gap: 8px; + box-shadow: 0 10px 22px rgba(15, 23, 42, 0.08); overflow: hidden; - padding-bottom: 8px; + position: relative; } -.material-card img { - aspect-ratio: 1; +.reference-card img { background: #e2e8f0; + height: 100%; object-fit: cover; width: 100%; } -.material-card button { - justify-self: center; - padding: 6px 10px; +.reference-card button { + align-items: center; + background: rgba(15, 23, 42, 0.72); + border-radius: 999px; + display: grid; + height: 24px; + padding: 0; + place-items: center; + position: absolute; + right: 7px; + top: 7px; + width: 24px; +} + +.reference-card > span { + align-items: center; + background: rgba(255, 255, 255, 0.88); + border-radius: 999px; + bottom: 7px; + color: #334155; + display: grid; + font-size: 12px; + font-weight: 900; + height: 24px; + left: 7px; + place-items: center; + position: absolute; + width: 24px; } .generate-button { - background: linear-gradient(135deg, #2563eb, #7c3aed); + background: linear-gradient(135deg, #2563eb, #7c3aed 58%, #f97316); border-radius: 18px; + box-shadow: 0 18px 36px rgba(37, 99, 235, 0.28); font-size: 17px; padding: 15px 18px; } .progress-card { align-items: flex-start; - background: #f8fafc; - border: 1px solid #e2e8f0; + background: rgba(248, 250, 252, 0.82); + border: 1px solid rgba(226, 232, 240, 0.92); border-radius: 22px; display: flex; gap: 14px; padding: 15px; } +.result-progress { + margin-top: auto; +} + .spinner { border: 3px solid rgba(148, 163, 184, 0.32); border-top-color: #2563eb; @@ -374,13 +499,21 @@ small { .empty-state { align-items: center; - border: 1px dashed #cbd5e1; + background: linear-gradient(180deg, rgba(248, 250, 252, 0.5), rgba(255, 255, 255, 0.76)); + border: 1px dashed rgba(148, 163, 184, 0.68); border-radius: 26px; color: #64748b; display: grid; + flex: 1; min-height: 420px; place-items: center; text-align: center; + margin-bottom: 16px; +} + +.empty-state > div { + display: grid; + place-items: center; } .empty-state div { @@ -390,17 +523,22 @@ small { margin-bottom: 8px; } +.empty-state p { + margin: 0; +} + .image-grid { display: grid; gap: 16px; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); + margin-bottom: 18px; } .image-card { background: #ffffff; - border: 1px solid #e2e8f0; - border-radius: 22px; - box-shadow: 0 16px 40px rgba(15, 23, 42, 0.08); + border: 1px solid rgba(226, 232, 240, 0.9); + border-radius: 24px; + box-shadow: 0 18px 46px rgba(15, 23, 42, 0.1); overflow: hidden; } @@ -487,6 +625,11 @@ small { gap: 10px; } +.drawer-actions button { + border-radius: 12px; + padding: 8px 14px; +} + .saved-providers { border-top: 1px solid #e2e8f0; display: grid; @@ -536,28 +679,11 @@ small { } @media (max-width: 980px) { - .workspace { - grid-template-columns: 1fr; - } - - .result-card { - min-height: 420px; - } + .workspace { grid-template-columns: minmax(360px, 440px) minmax(0, 1fr); } } @media (max-width: 720px) { - .app-shell { - padding: 12px; - } - - .topbar, .topbar-actions { - align-items: stretch; - flex-direction: column; - } - - .grid.two, .provider-item { - grid-template-columns: 1fr; - } + .grid.two, .provider-item { grid-template-columns: 1fr; } } .params-card { @@ -565,37 +691,18 @@ small { border: 1px solid #e2e8f0; border-radius: 22px; display: grid; - gap: 12px; - padding: 14px; + gap: 8px; + padding: 10px; } -.segmented { - background: #eef2f7; - border-radius: 16px; - display: grid; +.compact-field { gap: 6px; - grid-template-columns: repeat(3, 1fr); - padding: 6px; } -.segmented.compact { - grid-template-columns: repeat(4, 1fr); -} - -.segmented button { - background: transparent; - color: #64748b; - padding: 9px 8px; -} - -.segmented button:hover:not(:disabled) { - box-shadow: none; -} - -.segmented button.active { - background: #ffffff; - box-shadow: 0 8px 18px rgba(15, 23, 42, 0.08); - color: #1d4ed8; +.compact-field input, +.compact-field select { + border-radius: 14px; + padding: 10px 12px; } .image-preview-button { diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/src/vite-env.d.ts @@ -0,0 +1 @@ +///