From 6928d9250da805c0d9524083dec2c5465a626359 Mon Sep 17 00:00:00 2001 From: wonder Date: Sun, 24 May 2026 21:07:29 +0800 Subject: [PATCH 01/11] =?UTF-8?q?feat(theme):=20CSS=20=E5=8F=98=E9=87=8F?= =?UTF-8?q?=E9=87=8D=E6=9E=84=E4=B8=BA=E6=9A=97=E8=89=B2/=E6=B5=85?= =?UTF-8?q?=E8=89=B2=E5=8F=8C=E4=B8=BB=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将 :root 硬编码的深色变量拆分为 [data-theme="light"] 和 [data-theme="dark"] 两套主题块,新增导航栏、骨架屏、Toast 等预留变量,添加页面入场动画。 --- frontend/src/styles/global.css | 76 ++++++++++++++++++++++++++++++++-- 1 file changed, 73 insertions(+), 3 deletions(-) diff --git a/frontend/src/styles/global.css b/frontend/src/styles/global.css index dddaa4c..5ba89c9 100644 --- a/frontend/src/styles/global.css +++ b/frontend/src/styles/global.css @@ -1,4 +1,30 @@ -:root { +[data-theme="light"] { + --bg-primary: #f8f9fa; + --bg-secondary: #ffffff; + --bg-card: #ffffff; + --bg-input: #f1f3f5; + --text-primary: #212529; + --text-secondary: #6c757d; + --text-muted: #adb5bd; + --accent: #e94560; + --accent-hover: #d63851; + --accent-dim: rgba(233, 69, 96, 0.1); + --success: #2b8a3e; + --warning: #e67700; + --error: #c92a2a; + --border: #dee2e6; + --radius: 8px; + --radius-lg: 12px; + --shadow: 0 1px 3px rgba(0, 0, 0, 0.08); + --shadow-lg: 0 4px 12px rgba(0, 0, 0, 0.1); + --nav-bg: #ffffff; + --nav-border: #e9ecef; + --skeleton-base: #e9ecef; + --skeleton-shine: #f8f9fa; + --toast-bg: #ffffff; +} + +[data-theme="dark"] { --bg-primary: #1a1a2e; --bg-secondary: #16213e; --bg-card: #0f3460; @@ -15,6 +41,13 @@ --border: #2a2a4a; --radius: 8px; --radius-lg: 12px; + --shadow: 0 1px 3px rgba(0, 0, 0, 0.3); + --shadow-lg: 0 4px 12px rgba(0, 0, 0, 0.4); + --nav-bg: #16213e; + --nav-border: #2a2a4a; + --skeleton-base: #2a2a4a; + --skeleton-shine: #3a3a5a; + --toast-bg: #1e1e3a; } *, @@ -32,11 +65,13 @@ body { color: var(--text-primary); line-height: 1.6; min-height: 100vh; + transition: background-color 0.3s ease, color 0.3s ease; } a { color: var(--accent); text-decoration: none; + transition: color 0.2s ease; } a:hover { @@ -60,13 +95,14 @@ select { border-radius: var(--radius); padding: 10px 14px; outline: none; - transition: border-color 0.2s; + transition: border-color 0.2s, box-shadow 0.2s; } input:focus, textarea:focus, select:focus { border-color: var(--accent); + box-shadow: 0 0 0 3px var(--accent-dim); } button { @@ -75,7 +111,7 @@ button { border-radius: var(--radius); padding: 10px 20px; font-weight: 600; - transition: all 0.2s; + transition: all 0.2s ease; } button:disabled { @@ -92,6 +128,10 @@ button:disabled { background: var(--accent-hover); } +.btn-primary:active:not(:disabled) { + transform: scale(0.97); +} + .btn-secondary { background: transparent; color: var(--text-primary); @@ -101,6 +141,11 @@ button:disabled { .btn-secondary:hover:not(:disabled) { border-color: var(--accent); color: var(--accent); + background: var(--accent-dim); +} + +.btn-secondary:active:not(:disabled) { + transform: scale(0.97); } .card { @@ -108,6 +153,8 @@ button:disabled { border: 1px solid var(--border); border-radius: var(--radius-lg); padding: 24px; + box-shadow: var(--shadow); + transition: box-shadow 0.2s ease, border-color 0.2s ease; } .container { @@ -115,3 +162,26 @@ button:disabled { margin: 0 auto; padding: 0 20px; } + +.table-row-hover { + transition: background-color 0.15s ease; +} + +.table-row-hover:hover { + background: var(--accent-dim); +} + +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(8px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.page-enter { + animation: fadeIn 0.3s ease forwards; +} From dab9bc97509c58bed307b6ebfaa8f3ca4f038ce2 Mon Sep 17 00:00:00 2001 From: wonder Date: Sun, 24 May 2026 21:08:02 +0800 Subject: [PATCH 02/11] =?UTF-8?q?feat(theme):=20Zustand=20=E4=B8=BB?= =?UTF-8?q?=E9=A2=98=20store=EF=BC=8ClocalStorage=20=E6=8C=81=E4=B9=85?= =?UTF-8?q?=E5=8C=96=EF=BC=8C=E9=BB=98=E8=AE=A4=E6=B5=85=E8=89=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/stores/theme.ts | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 frontend/src/stores/theme.ts diff --git a/frontend/src/stores/theme.ts b/frontend/src/stores/theme.ts new file mode 100644 index 0000000..ed54a79 --- /dev/null +++ b/frontend/src/stores/theme.ts @@ -0,0 +1,22 @@ +import { create } from 'zustand' +import { persist } from 'zustand/middleware' + +type Theme = 'light' | 'dark' + +interface ThemeState { + theme: Theme + toggleTheme: () => void + setTheme: (t: Theme) => void +} + +export const useThemeStore = create()( + persist( + (set) => ({ + theme: 'light', + toggleTheme: () => + set((state) => ({ theme: state.theme === 'light' ? 'dark' : 'light' })), + setTheme: (t) => set({ theme: t }), + }), + { name: 'gen2d_theme' }, + ), +) From 323472ab2ba981711f7731a313cc2b81a9dd037a Mon Sep 17 00:00:00 2001 From: wonder Date: Sun, 24 May 2026 21:09:23 +0800 Subject: [PATCH 03/11] =?UTF-8?q?feat(theme):=20App=20=E5=88=9D=E5=A7=8B?= =?UTF-8?q?=E5=8C=96=E6=97=B6=E8=AE=BE=E7=BD=AE=20data-theme=20=E5=B1=9E?= =?UTF-8?q?=E6=80=A7=EF=BC=8C=E9=A6=96=E6=AC=A1=E6=B8=B2=E6=9F=93=E5=8D=B3?= =?UTF-8?q?=E4=B8=BA=E4=B8=BB=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/App.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index c3ffe69..1672697 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -2,14 +2,20 @@ import { useEffect } from 'react' import { RouterProvider } from 'react-router-dom' import { router } from './router' import { useAuthStore } from './stores/auth' +import { useThemeStore } from './stores/theme' import './styles/global.css' export default function App() { const loadFromStorage = useAuthStore(s => s.loadFromStorage) + const theme = useThemeStore(s => s.theme) useEffect(() => { loadFromStorage() }, [loadFromStorage]) + useEffect(() => { + document.documentElement.setAttribute('data-theme', theme) + }, [theme]) + return } From eac64105de323a27b5fa7f3937dcdb3434b2c72d Mon Sep 17 00:00:00 2001 From: wonder Date: Sun, 24 May 2026 21:12:23 +0800 Subject: [PATCH 04/11] =?UTF-8?q?feat(layout):=20=E8=AE=A4=E8=AF=81?= =?UTF-8?q?=E9=A1=B5=E9=9D=A2=E7=BB=9F=E4=B8=80=E5=AF=BC=E8=88=AA=E6=A0=8F?= =?UTF-8?q?=EF=BC=8C=E5=90=AB=E4=B8=BB=E9=A2=98=E5=88=87=E6=8D=A2=E4=B8=8E?= =?UTF-8?q?=E9=80=80=E5=87=BA=E6=8C=89=E9=92=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 Layout 组件作为认证路由的公共布局,提供 sticky 导航栏, 移除各页面重复的顶栏代码。 --- frontend/src/components/Layout.tsx | 64 +++++++++++++++++++++++++++++ frontend/src/pages/GeneratePage.tsx | 25 +---------- frontend/src/pages/ProjectPage.tsx | 22 +--------- frontend/src/pages/ResultPage.tsx | 23 +---------- frontend/src/router/index.tsx | 34 ++++++++------- 5 files changed, 88 insertions(+), 80 deletions(-) create mode 100644 frontend/src/components/Layout.tsx diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx new file mode 100644 index 0000000..abb2be5 --- /dev/null +++ b/frontend/src/components/Layout.tsx @@ -0,0 +1,64 @@ +import { Link, Outlet } from 'react-router-dom' +import { useAuthStore } from '../stores/auth' +import { useThemeStore } from '../stores/theme' + +export default function Layout() { + const { user, logout } = useAuthStore() + const { theme, toggleTheme } = useThemeStore() + + return ( + <> + +
+ +
+ + ) +} diff --git a/frontend/src/pages/GeneratePage.tsx b/frontend/src/pages/GeneratePage.tsx index 0fc55ce..868e553 100644 --- a/frontend/src/pages/GeneratePage.tsx +++ b/frontend/src/pages/GeneratePage.tsx @@ -1,5 +1,5 @@ import { useEffect } from 'react' -import { Link, useNavigate, useParams } from 'react-router-dom' +import { useNavigate, useParams } from 'react-router-dom' import { useTaskStore } from '../stores/task' import { useGenerationStore } from '../stores/generation' import GenerateForm from '../components/GenerateForm' @@ -46,28 +46,7 @@ export default function GeneratePage() { return (
- {/* 顶栏 */} -
-

新建生成

- - 返回工程 - -
+

新建生成

{/* 生成表单 */} {status === 'idle' || status === 'submitting' ? ( diff --git a/frontend/src/pages/ProjectPage.tsx b/frontend/src/pages/ProjectPage.tsx index 8dbefc8..df40981 100644 --- a/frontend/src/pages/ProjectPage.tsx +++ b/frontend/src/pages/ProjectPage.tsx @@ -1,7 +1,6 @@ import { useEffect, useState } from 'react' import { Link, useParams } from 'react-router-dom' import { useProjectStore } from '../stores/project' -import { useAuthStore } from '../stores/auth' import { getTasks } from '../api/project' import type { Task } from '../api/types' import StyleSelector from '../components/StyleSelector' @@ -15,7 +14,6 @@ const STATUS_LABELS: Record = { export default function ProjectPage() { const { projectId = 'proj-default' } = useParams() - const { user, logout } = useAuthStore() const { name, style, @@ -45,25 +43,7 @@ export default function ProjectPage() { return (
- {/* 顶栏 */} -
-

{loading ? '加载中...' : name}

-
- - {user?.username} - - -
-
+

{loading ? '加载中...' : name}

{/* 工程风格 */}
diff --git a/frontend/src/pages/ResultPage.tsx b/frontend/src/pages/ResultPage.tsx index 4a0bd62..83738b1 100644 --- a/frontend/src/pages/ResultPage.tsx +++ b/frontend/src/pages/ResultPage.tsx @@ -42,28 +42,7 @@ export default function ResultPage() { return (
- {/* 顶栏 */} -
-

生成结果

- - 返回工程 - -
+

生成结果

{/* 任务信息 */}
diff --git a/frontend/src/router/index.tsx b/frontend/src/router/index.tsx index f29091d..8c6f1c7 100644 --- a/frontend/src/router/index.tsx +++ b/frontend/src/router/index.tsx @@ -1,5 +1,6 @@ import { createBrowserRouter, Navigate, Outlet } from 'react-router-dom' import { useAuthStore } from '../stores/auth' +import Layout from '../components/Layout' import LoginPage from '../pages/LoginPage' import RegisterPage from '../pages/RegisterPage' import ProjectPage from '../pages/ProjectPage' @@ -25,20 +26,25 @@ export const router = createBrowserRouter([ element: , children: [ { - path: '/', - element: , - }, - { - path: '/projects/:projectId', - element: , - }, - { - path: '/projects/:projectId/generate', - element: , - }, - { - path: '/projects/:projectId/tasks/:taskId', - element: , + element: , + children: [ + { + path: '/', + element: , + }, + { + path: '/projects/:projectId', + element: , + }, + { + path: '/projects/:projectId/generate', + element: , + }, + { + path: '/projects/:projectId/tasks/:taskId', + element: , + }, + ], }, ], }, From c177cbaf82660e42c1371589290e0faebe1393a6 Mon Sep 17 00:00:00 2001 From: wonder Date: Sun, 24 May 2026 21:16:15 +0800 Subject: [PATCH 05/11] =?UTF-8?q?feat(prompt):=20AI=20=E6=8F=90=E7=A4=BA?= =?UTF-8?q?=E8=AF=8D=E4=BC=98=E5=8C=96=E5=8F=8C=E6=A0=8F=E4=BA=A4=E4=BA=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 prompt API 模块调用后端 /api/v1/prompt/optimize,PromptEditor 重构为左右双栏布局支持原始/优化提示词对比编辑,默认开启 AI 优化, 用户可关闭开关或手动编辑优化结果后再提交生成。 --- frontend/src/api/prompt.ts | 35 +++++++ .../src/components/GenerateForm.module.css | 98 +++++++++++++++++++ frontend/src/components/GenerateForm.tsx | 58 +++++++++-- frontend/src/components/PromptEditor.tsx | 79 +++++++++++++-- frontend/src/pages/GeneratePage.tsx | 6 +- frontend/src/stores/task.ts | 50 +++++++++- 6 files changed, 304 insertions(+), 22 deletions(-) create mode 100644 frontend/src/api/prompt.ts create mode 100644 frontend/src/components/GenerateForm.module.css diff --git a/frontend/src/api/prompt.ts b/frontend/src/api/prompt.ts new file mode 100644 index 0000000..0d41baf --- /dev/null +++ b/frontend/src/api/prompt.ts @@ -0,0 +1,35 @@ +import { post } from './client' +import { STYLE_CATEGORIES } from '../utils/style' + +interface OptimizePromptParams { + tags: string[] + assetType: string + prompt: string + userNote?: string +} + +interface OptimizePromptResponse { + prompt: string + rawText: string +} + +/** + * 从风格键值对中提取标签的中文名称作为 tags + */ +export function extractTags(style: Record): string[] { + return STYLE_CATEGORIES.flatMap(cat => { + const value = style[cat.key] + if (!value) return [] + const option = cat.options.find(o => o.value === value) + return option ? [option.label] : [] + }) +} + +/** + * 调用后端提示词优化接口 + */ +export async function optimizePrompt( + params: OptimizePromptParams, +): Promise { + return post('/api/v1/prompt/optimize', params) +} diff --git a/frontend/src/components/GenerateForm.module.css b/frontend/src/components/GenerateForm.module.css new file mode 100644 index 0000000..11efb51 --- /dev/null +++ b/frontend/src/components/GenerateForm.module.css @@ -0,0 +1,98 @@ +.promptPanels { + display: flex; + gap: 16px; +} + +.panel { + flex: 1; + display: flex; + flex-direction: column; + gap: 8px; +} + +.panelSingle { + composes: panel; + flex: 1; +} + +.panelLabel { + font-size: 13px; + font-weight: 600; + color: var(--text-secondary); +} + +.optimizedBadge { + font-size: 11px; + padding: 2px 8px; + border-radius: 10px; + background: var(--accent-dim); + color: var(--accent); + font-weight: 600; +} + +.textarea { + resize: vertical; + width: 100%; + min-height: 120px; +} + +.toggleRow { + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 4px; +} + +.toggle { + position: relative; + width: 40px; + height: 22px; + border-radius: 11px; + border: none; + cursor: pointer; + transition: background-color 0.2s; + padding: 0; + flex-shrink: 0; +} + +.toggleOn { + background: var(--accent); +} + +.toggleOff { + background: var(--border); +} + +.toggleKnob { + position: absolute; + top: 2px; + width: 18px; + height: 18px; + border-radius: 50%; + background: #fff; + transition: left 0.2s; +} + +.toggleKnobOn { + left: 20px; +} + +.toggleKnobOff { + left: 2px; +} + +.toggleLabel { + font-size: 13px; + color: var(--text-secondary); +} + +.optimizeBtn { + padding: 6px 16px; + font-size: 13px; +} + +@media (max-width: 640px) { + .promptPanels { + flex-direction: column; + } +} diff --git a/frontend/src/components/GenerateForm.tsx b/frontend/src/components/GenerateForm.tsx index e3c2ec0..40a05ec 100644 --- a/frontend/src/components/GenerateForm.tsx +++ b/frontend/src/components/GenerateForm.tsx @@ -13,7 +13,7 @@ const ASSET_TYPES: { value: AssetType; label: string }[] = [ ] interface GenerateFormProps { - onSubmit: () => void + onSubmit: (prompt: string) => void submitting: boolean } @@ -24,13 +24,32 @@ export default function GenerateForm({ onSubmit, submitting }: GenerateFormProps assetType, taskStyle, params, + enableAI, + optimizedPrompt, + isOptimizing, setPrompt, setAssetType, toggleTaskStyle, setParams, + setEnableAI, + setOptimizedPrompt, + runOptimize, } = useTaskStore() const mergedStyle = mergeStyles(projectStyle, taskStyle) + const finalPrompt = enableAI && optimizedPrompt ? optimizedPrompt : prompt + + const handleOptimize = async () => { + try { + await runOptimize(projectStyle) + } catch { + // error handled in store + } + } + + const handleSubmit = () => { + onSubmit(finalPrompt) + } return (
@@ -62,16 +81,41 @@ export default function GenerateForm({ onSubmit, submitting }: GenerateFormProps
- {/* 提示词 */} + {/* 提示词(含 AI 优化) */}
-

- 提示词 -

+
+

+ 提示词 +

+ {enableAI && ( + + )} +
@@ -113,8 +157,8 @@ export default function GenerateForm({ onSubmit, submitting }: GenerateFormProps