From c3e74c228a8a173c618963a70a9dfae353504891 Mon Sep 17 00:00:00 2001 From: saturn Date: Sat, 28 Mar 2026 18:58:21 +0800 Subject: [PATCH] style: polish UI and improve UX --- messages/en/assetHub.json | 1 + messages/en/home.json | 2 +- messages/en/novel-promotion.json | 2 +- messages/zh/assetHub.json | 1 + messages/zh/home.json | 2 +- messages/zh/novel-promotion.json | 2 +- src/app/[locale]/home/page.tsx | 228 +++++++--------- .../components/NovelInputStage.tsx | 207 ++++++++------- .../asset-hub/components/AssetGrid.tsx | 31 +++ src/components/home/TypewriterHero.tsx | 106 ++++++++ .../selectors/RatioStyleSelectors.tsx | 249 ++++++++++++++++-- .../story-input/StoryInputComposer.tsx | 170 ++++++++++++ src/lib/style-presets.ts | 16 ++ .../textarea-height.ts} | 1 + tests/unit/components/asset-grid.test.ts | 158 +++++++++++ .../components/ratio-style-selectors.test.ts | 107 ++++++++ .../components/story-input-composer.test.ts | 51 ++++ tests/unit/home/quick-start-textarea.test.ts | 28 +- .../novel-promotion/novel-input-stage.test.ts | 87 ++++++ 19 files changed, 1182 insertions(+), 267 deletions(-) create mode 100644 src/components/home/TypewriterHero.tsx create mode 100644 src/components/story-input/StoryInputComposer.tsx create mode 100644 src/lib/style-presets.ts rename src/lib/{home/quick-start-textarea.ts => ui/textarea-height.ts} (89%) create mode 100644 tests/unit/components/asset-grid.test.ts create mode 100644 tests/unit/components/ratio-style-selectors.test.ts create mode 100644 tests/unit/components/story-input-composer.test.ts create mode 100644 tests/unit/novel-promotion/novel-input-stage.test.ts diff --git a/messages/en/assetHub.json b/messages/en/assetHub.json index 281eb6e..fe9de8a 100644 --- a/messages/en/assetHub.json +++ b/messages/en/assetHub.json @@ -22,6 +22,7 @@ "downloadSuccess": "Download Complete", "downloadFailed": "Download Failed", "downloadEmpty": "No image assets to download", + "filteredEmptyHint": "Click \"New Asset\" to add assets", "newFolder": "New Folder", "editFolder": "Edit Folder", "deleteFolder": "Delete Folder", diff --git a/messages/en/home.json b/messages/en/home.json index 6bda9ce..7afccab 100644 --- a/messages/en/home.json +++ b/messages/en/home.json @@ -1,5 +1,5 @@ { - "title": "Quick Start", + "title": "From Inspiration to Screen", "subtitle": "Describe your story and let AI generate cinematic short dramas", "inputPlaceholder": "Enter your story idea, novel excerpt, or script outline...", "startCreation": "Start Creating", diff --git a/messages/en/novel-promotion.json b/messages/en/novel-promotion.json index 9ce454f..aabec27 100644 --- a/messages/en/novel-promotion.json +++ b/messages/en/novel-promotion.json @@ -173,4 +173,4 @@ "confirm": "Continue and Clear", "cancel": "Cancel" } -} \ No newline at end of file +} diff --git a/messages/zh/assetHub.json b/messages/zh/assetHub.json index 0088362..2c3708f 100644 --- a/messages/zh/assetHub.json +++ b/messages/zh/assetHub.json @@ -22,6 +22,7 @@ "downloadSuccess": "下载完成", "downloadFailed": "下载失败", "downloadEmpty": "当前没有可下载的图片资产", + "filteredEmptyHint": "点击新建资产添加资产", "newFolder": "新建文件夹", "editFolder": "编辑文件夹", "deleteFolder": "删除文件夹", diff --git a/messages/zh/home.json b/messages/zh/home.json index c938607..75c474c 100644 --- a/messages/zh/home.json +++ b/messages/zh/home.json @@ -1,5 +1,5 @@ { - "title": "快速开始", + "title": "从灵感到银幕", "subtitle": "描述你想要创作的故事,AI 为你智能生成影视短剧", "inputPlaceholder": "输入你的故事创意、小说片段或剧本大纲...", "startCreation": "开始创作", diff --git a/messages/zh/novel-promotion.json b/messages/zh/novel-promotion.json index e86ab73..bb3c6c2 100644 --- a/messages/zh/novel-promotion.json +++ b/messages/zh/novel-promotion.json @@ -173,4 +173,4 @@ "confirm": "继续并清空", "cancel": "取消" } -} \ No newline at end of file +} diff --git a/src/app/[locale]/home/page.tsx b/src/app/[locale]/home/page.tsx index 4ee90cc..168d7f0 100644 --- a/src/app/[locale]/home/page.tsx +++ b/src/app/[locale]/home/page.tsx @@ -4,21 +4,20 @@ * 首页 - 创作中心 * 用户登录后的主入口页面:快速创作 + 最近项目 */ -import { useState, useEffect, useCallback, useMemo, useRef } from 'react' +import { useState, useEffect, useCallback, useMemo } from 'react' import { useSession } from 'next-auth/react' import { useTranslations } from 'next-intl' import Navbar from '@/components/Navbar' import { AppIcon, IconGradientDefs } from '@/components/ui/icons' -import { RatioSelector, StyleSelector } from '@/components/selectors/RatioStyleSelectors' +import StoryInputComposer from '@/components/story-input/StoryInputComposer' +import TypewriterHero from '@/components/home/TypewriterHero' import { ART_STYLES, VIDEO_RATIOS } from '@/lib/constants' +import { DEFAULT_STYLE_PRESET_VALUE, STYLE_PRESETS } from '@/lib/style-presets' import { Link, useRouter } from '@/i18n/navigation' import { apiFetch } from '@/lib/api-fetch' import { expandHomeStory } from '@/lib/home/ai-story-expand' import { createHomeProjectLaunch } from '@/lib/home/create-project-launch' -import { - HOME_QUICK_START_MIN_ROWS, - resolveTextareaTargetHeight, -} from '@/lib/home/quick-start-textarea' +import { HOME_QUICK_START_MIN_ROWS } from '@/lib/ui/textarea-height' import AiWriteModal from '@/components/home/AiWriteModal' interface ProjectStats { @@ -51,48 +50,10 @@ export default function HomePage() { const [inputValue, setInputValue] = useState('') const [videoRatio, setVideoRatio] = useState('9:16') const [artStyle, setArtStyle] = useState('american-comic') + const [stylePresetValue, setStylePresetValue] = useState(DEFAULT_STYLE_PRESET_VALUE) const [createLoading, setCreateLoading] = useState(false) const [aiWriteOpen, setAiWriteOpen] = useState(false) const [aiWriteLoading, setAiWriteLoading] = useState(false) - const textareaRef = useRef(null) - const textareaMinHeightRef = useRef(null) - - // textarea 自适应高度(rAF 分帧动画) - const autoResizeTextarea = useCallback(() => { - const el = textareaRef.current - if (!el) return - const maxH = window.innerHeight * 0.5 - const oldH = el.offsetHeight - const oldScrollTop = el.scrollTop - if (textareaMinHeightRef.current === null && oldH > 0) { - textareaMinHeightRef.current = oldH - } - const minH = textareaMinHeightRef.current ?? oldH - - // 同步:测量真实高度(不改 overflow,避免 scrollTop 被重置) - el.style.transition = 'none' - el.style.height = 'auto' - const scrollH = el.scrollHeight - const targetH = resolveTextareaTargetHeight({ - minHeight: minH, - maxHeight: maxH, - scrollHeight: scrollH, - }) - el.style.height = `${oldH}px` - el.scrollTop = oldScrollTop - - // 下一帧:开启 transition → 动画到目标高度 - requestAnimationFrame(() => { - el.scrollTop = oldScrollTop - el.style.transition = 'height 200ms ease-out' - el.style.height = `${targetH}px` - el.style.overflowY = scrollH > maxH ? 'auto' : 'hidden' - }) - }, []) - - useEffect(() => { - autoResizeTextarea() - }, [inputValue, autoResizeTextarea]) // 鉴权 useEffect(() => { @@ -183,7 +144,6 @@ export default function HomePage() { () => ART_STYLES.map((s) => ({ ...s, recommended: s.value === 'realistic' })), [] ) - // 时间格式化 const formatTimeAgo = (dateString: string): string => { const diffMs = Date.now() - new Date(dateString).getTime() @@ -228,97 +188,105 @@ export default function HomePage() { 45% { transform: translate(-15px, -20px) scale(1.15); opacity: 0.7; } 70% { transform: translate(10px, -10px) scale(1); opacity: 0.35; } } + @keyframes bracket-breathe { + 0%, 70%, 100% { opacity: 0.2; } + 75%, 90% { opacity: 0.6; } + } `} -
-
-

- ✨ {t('title')} -

-

{t('subtitle')}

-
+
- {/* 呼吸光晕 + 输入区域 */} -
-
-
-
+ {/* ─── 取景器整体包裹:标题 + 输入框 ─── */} +
+ {/* 四角校准线 */} + + + + -
-