Files
omniai-web/src/features/more/MorePage.tsx
T
ludan 52677e33f1 feat: 首页响应式视觉升级与全局UI细节打磨
本次提交包含以下改进:

## 1. 首页轮播卡片响应式重构 (HomePage.tsx + home.css)
- 将旋转木马卡片偏移量从固定px值改为clamp()流式单位,随视口宽度自适应缩放
- 使用calc(0px - ...)替代乘法计算方向偏移,兼容CSS变量传递
- 轮播舞台新增mask-image渐变遮罩,边缘卡片自然淡出
- 非激活卡片增加saturate/brightness滤镜,强化主次视觉层级
- 激活卡与非激活卡分别设置图片filter效果
- 移除旧carousel-card-label样式
- 多断点适配:1200px/980px/720px/480px逐级调整卡片尺寸和舞台高度

## 2. 首页入口按钮重设计 (HomePage.tsx + home.css)
- 按钮文案从'新手/老手/电商'改为'快速生成/专业创作/电商出图'
- 每个按钮新增small副标题('新手友好'/'画布工作流'/'商品视觉')
- 主按钮(专业创作)使用渐变绿色背景+发光阴影,新建--primary small样式
- 普通按钮玻璃态背景+内阴影,hover绿色边框高亮
- 720px以下单列全宽布局,按钮居中

## 3. 首页全页视觉强化 (home.css)
- Scrim层三重渐变叠加+radial光晕
- Hero区域文字text-shadow + text-wrap: balance排版
- Feature页面::before叠加渐变遮罩
- Feature Visual卡片增加边框/阴影/背景三层嵌套
- Experience区域斜向分割线装饰背景
- Cookie Consent弹窗玻璃态重设计,移动端自适应

## 4. 首页工具盒区域打磨 (toolbox.css)
- 全新CSS变量(--toolbox-radius-card/inner)
- 工具盒整体深色渐变背景+radial光晕
- Shell容器max-width + clamp流式padding
- 左侧品牌区域标题/brand-icon/subtitle重设计
- 工具列表项、工作流卡片统一玻璃态风格
- 工具卡片hover上浮4px+绿色边框+阴影增强
- @media: 1160px/980px/680px/420px四断点响应式

## 5. 工具盒卡片布局简化 (MorePage.tsx + more.css)
- 核心工具卡片移除独立icon区域,改为单列网格布局
- 普通工具卡片隐藏.more-card__icon(近期记录除外)
- 预览图aspect-ratio从16/9改为4/3,内边距优化
- 移动端移除featured-icon相关样式

## 6. 脚本评审Showcase响应式改造 (script-review-showcase.css)
- 主容器从@media切换为@container查询,跟随父容器自适应
- 新增880px/720px/560px三档container断点
- 图表列在720px以下改为水平进度条布局(bar从垂直改水平)
- 图表列增加卡片边框/圆角/背景
- 品牌区域、评分标签、流程卡片逐级压缩
- @media保留外层padding控制

## 7. 通知中心UI修复 (dark-green.css)
- notification-center改为inline-flex定位锚点
- 面板改为absolute+flex列布局,修复定位偏移
- 列表flex自适应高度+overscroll-behavior: contain
- 移动端面板右偏移clamp适配,箭头位置同步
- 高度单位从vh改为dvh,避免移动浏览器地址栏干扰
2026-06-09 14:22:37 +08:00

430 lines
16 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import {
CameraOutlined,
ClockCircleOutlined,
ColumnWidthOutlined,
CustomerServiceOutlined,
DashboardOutlined,
DeleteOutlined,
EditOutlined,
HighlightOutlined,
MessageOutlined,
SwapOutlined,
ThunderboltOutlined,
VideoCameraOutlined,
} from "@ant-design/icons";
import type { CSSProperties, ReactNode } from "react";
import { useCallback, useEffect, useState } from "react";
import "../../styles/pages/more.css";
import type { WebImageWorkbenchTool, WebViewKey } from "../../types";
interface MorePageProps {
onSelectView?: (view: WebViewKey) => void;
onOpenImageTool?: (tool: WebImageWorkbenchTool) => void;
}
type ToolCategory = "image" | "video";
type FilterKey = "all" | ToolCategory | "upcoming";
interface MoreTool {
id: string;
title: string;
text: string;
useCase: string;
tags: string[];
icon: ReactNode;
category: ToolCategory;
target?: WebViewKey;
imageTool?: WebImageWorkbenchTool;
ready: boolean;
badge?: string;
}
const toolPreviewImages: Record<string, string> = {
inpaint: "https://stringtest.oss-cn-hangzhou.aliyuncs.com/toolbox/images/%E5%B1%80%E9%83%A8%E9%87%8D%E7%BB%98.PNG",
camera: "https://stringtest.oss-cn-hangzhou.aliyuncs.com/toolbox/images/%E9%95%9C%E5%A4%B4%E5%AE%9E%E9%AA%8C%E5%AE%A4.PNG",
upscale: "https://stringtest.oss-cn-hangzhou.aliyuncs.com/toolbox/images/%E5%88%86%E8%BE%A8%E7%8E%87%E6%8F%90%E5%8D%87.PNG",
watermarkRemoval: "https://stringtest.oss-cn-hangzhou.aliyuncs.com/toolbox/images/%E5%8E%BB%E6%B0%B4%E5%8D%B0.PNG",
dialogGenerator: "https://stringtest.oss-cn-hangzhou.aliyuncs.com/toolbox/images/%E4%BA%A4%E4%BA%92%E5%BC%8F%E5%AF%B9%E8%AF%9D%E6%A1%86%E7%94%9F%E6%88%90%E5%99%A8.PNG",
subtitleRemoval: "https://stringtest.oss-cn-hangzhou.aliyuncs.com/toolbox/images/%E5%AD%97%E5%B9%95%E5%8E%BB%E9%99%A4.PNG",
characterMix: "https://stringtest.oss-cn-hangzhou.aliyuncs.com/toolbox/images/%E8%A7%92%E8%89%B2%E8%BF%81%E7%A7%BB.PNG",
avatarConsole: "https://stringtest.oss-cn-hangzhou.aliyuncs.com/toolbox/images/%E6%95%B0%E5%AD%97%E4%BA%BA%E6%8E%A7%E5%88%B6%E5%8F%B0.PNG",
};
function ToolPreviewPanel({ toolId }: { toolId: string }) {
const imageUrl = toolPreviewImages[toolId];
if (!imageUrl) return null;
return (
<span className="more-card__preview" aria-hidden="true">
<span className="more-card__preview-frame">
<img src={imageUrl} alt="" loading="lazy" decoding="async" />
</span>
<img className="more-card__preview-popover" src={imageUrl} alt="" loading="lazy" decoding="async" />
</span>
);
}
function getPreviewClassName(toolId: string) {
return toolPreviewImages[toolId] ? " more-card--has-preview" : " more-card--no-preview";
}
const tools: MoreTool[] = [
{
id: "workbench",
title: "图片工作台",
text: "融合、修复、局部增强",
useCase: "适合商品图精修、创意合成和局部画面重做",
tags: ["热门", "一站式", "商品图"],
icon: <EditOutlined />,
category: "image",
imageTool: "workbench",
ready: true,
},
{
id: "inpaint",
title: "局部重绘",
text: "修掉瑕疵、替换物体、重做局部画面",
useCase: "适合快速处理商品瑕疵、人物细节和背景杂物",
tags: ["新手推荐", "精修"],
icon: <HighlightOutlined />,
category: "image",
imageTool: "inpaint",
ready: true,
},
{
id: "camera",
title: "镜头实验室",
text: "快速生成俯拍、特写、广角等商业镜头",
useCase: "适合做产品主图、种草图和不同机位方案",
tags: ["电商常用", "镜头"],
icon: <CameraOutlined />,
category: "image",
imageTool: "camera",
ready: true,
},
{
id: "upscale",
title: "分辨率提升",
text: "把低清图片或视频提升到可交付质感",
useCase: "适合修复旧素材、放大商品图和增强短视频清晰度",
tags: ["高清", "交付前"],
icon: <ColumnWidthOutlined />,
category: "image",
target: "resolutionUpscale",
ready: true,
},
{
id: "watermarkRemoval",
title: "去水印",
text: "智能去除图片水印、文字和遮挡元素",
useCase: "适合整理素材、清理参考图和恢复画面干净度",
tags: ["素材清理", "高频"],
icon: <DeleteOutlined />,
category: "image",
target: "watermarkRemoval",
ready: true,
},
{
id: "dialogGenerator",
title: "交互式对话框生成器",
text: "上传背景图,快速制作可拖拽编辑的对话框",
useCase: "适合剧情海报、社媒截图和角色对白设计",
tags: ["内容创作", "可编辑"],
icon: <MessageOutlined />,
category: "image",
target: "dialogGenerator",
ready: true,
},
{
id: "subtitleRemoval",
title: "字幕去除",
text: "擦除视频字幕,让画面重新变干净",
useCase: "适合二创前素材整理、短视频重剪和画面修复",
tags: ["视频增强", "素材清理"],
icon: <DeleteOutlined />,
category: "video",
target: "subtitleRemoval",
ready: true,
},
{
id: "digitalHuman",
title: "数字人",
text: "用一张人像和音频生成口播视频",
useCase: "适合品牌讲解、课程口播和带货短视频",
tags: ["热门", "口播", "视频"],
icon: <CustomerServiceOutlined />,
category: "video",
target: "digitalHuman",
ready: true,
},
{
id: "characterMix",
title: "角色迁移",
text: "把人物图迁移到参考视频的动作里",
useCase: "适合角色短片、动作复刻和虚拟人内容生产",
tags: ["角色视频", "动作"],
icon: <SwapOutlined />,
category: "video",
target: "characterMix",
ready: true,
},
{
id: "avatarConsole",
title: "数字人控制台",
text: "管理形象、播报、互动与接入配置",
useCase: "适合持续运营数字人、配置品牌形象和复用口播模板",
tags: ["运营台", "企业"],
icon: <DashboardOutlined />,
category: "video",
target: "avatarConsole",
ready: true,
},
];
const categoryLabels: Record<ToolCategory, string> = {
image: "图像创作",
video: "视频创作",
};
const categoryIcons: Record<ToolCategory, ReactNode> = {
image: <EditOutlined />,
video: <VideoCameraOutlined />,
};
const filters: { key: FilterKey; label: string }[] = [
{ key: "all", label: "全部" },
{ key: "image", label: "图像" },
{ key: "video", label: "视频" },
{ key: "upcoming", label: "即将上线" },
];
const coreToolIds = new Set(["workbench", "inpaint", "watermarkRemoval"]);
const coreToolGradients: Record<string, string> = {
workbench: "linear-gradient(135deg, rgba(99, 102, 241, 0.12), rgba(139, 92, 246, 0.06))",
inpaint: "linear-gradient(135deg, rgba(var(--accent-rgb), 0.16), rgba(var(--accent-rgb), 0.055))",
watermarkRemoval: "linear-gradient(135deg, rgba(16, 185, 129, 0.13), rgba(var(--accent-rgb), 0.055))",
};
const coreToolSteps: Record<string, string[]> = {
workbench: ["上传素材", "局部修复", "高清导出"],
inpaint: ["选定区域", "描述修改", "生成结果"],
watermarkRemoval: ["上传素材", "智能识别", "干净导出"],
};
const RECENT_STORAGE_KEY = "omniai:more-recent-tools";
const MAX_RECENT = 4;
function getRecentToolIds(): string[] {
try {
const raw = localStorage.getItem(RECENT_STORAGE_KEY);
return raw ? JSON.parse(raw) : [];
} catch {
return [];
}
}
function pushRecentToolId(id: string) {
const ids = getRecentToolIds().filter((x) => x !== id);
ids.unshift(id);
localStorage.setItem(RECENT_STORAGE_KEY, JSON.stringify(ids.slice(0, MAX_RECENT)));
}
function MorePage({ onSelectView, onOpenImageTool }: MorePageProps) {
const [filter, setFilter] = useState<FilterKey>("all");
const [recentIds, setRecentIds] = useState<string[]>(getRecentToolIds);
useEffect(() => {
setRecentIds(getRecentToolIds());
}, []);
const openTool = useCallback((tool: MoreTool) => {
if (!tool.ready) return;
pushRecentToolId(tool.id);
setRecentIds(getRecentToolIds());
if (tool.imageTool && onOpenImageTool) {
onOpenImageTool(tool.imageTool);
return;
}
if (tool.target && onSelectView) {
onSelectView(tool.target);
}
}, [onOpenImageTool, onSelectView]);
const filteredTools = tools.filter((tool) => {
if (coreToolIds.has(tool.id)) return false;
if (filter === "all") return true;
if (filter === "upcoming") return !tool.ready;
return tool.category === filter;
});
const filterCounts: Record<FilterKey, number> = {
all: tools.filter((tool) => !coreToolIds.has(tool.id)).length,
image: tools.filter((tool) => !coreToolIds.has(tool.id) && tool.category === "image").length,
video: tools.filter((tool) => !coreToolIds.has(tool.id) && tool.category === "video").length,
upcoming: tools.filter((tool) => !coreToolIds.has(tool.id) && !tool.ready).length,
};
const recentTools = recentIds
.map((id) => tools.find((tool) => tool.id === id))
.filter((tool): tool is MoreTool => Boolean(tool) && (tool?.ready ?? false));
const coreTools = tools.filter((tool) => coreToolIds.has(tool.id));
const groupedTools = filteredTools.reduce<Record<ToolCategory, MoreTool[]>>((acc, tool) => {
if (!acc[tool.category]) acc[tool.category] = [];
acc[tool.category].push(tool);
return acc;
}, {} as Record<ToolCategory, MoreTool[]>);
const activeFilterLabel = filters.find((item) => item.key === filter)?.label ?? "全部";
const hasGroupedTools = (["image", "video"] as ToolCategory[]).some((cat) => groupedTools[cat]?.length);
return (
<div className="more-page-v2">
<header className="more-page-v2__header">
<div className="more-page-v2__title-group">
<span className="more-page-v2__eyebrow">AI Tool Hub</span>
<h1></h1>
</div>
<div className="more-page-v2__header-meta" aria-label="工具盒概览">
<span>{tools.filter((tool) => tool.ready).length} </span>
<span>{coreTools.length} </span>
</div>
<nav className="more-page-v2__filters" aria-label="工具分类筛选">
{filters.map((item) => (
<button
key={item.key}
type="button"
className={filter === item.key ? "is-active" : ""}
aria-pressed={filter === item.key}
onClick={() => setFilter(item.key)}
>
<span>{item.label}</span>
<em>{filterCounts[item.key]}</em>
</button>
))}
</nav>
</header>
<div className="more-page-v2__scroll">
{recentTools.length > 0 && filter === "all" && (
<section className="more-page-v2__section">
<h2 className="more-page-v2__section-title">
<ClockCircleOutlined /> 使
<span>使</span>
</h2>
<div className="more-page-v2__recent-row">
{recentTools.map((tool) => (
<button
key={tool.id}
type="button"
className="more-card more-card--recent"
aria-label={`打开最近使用工具:${tool.title}`}
onClick={() => openTool(tool)}
>
<span className="more-card__icon">{tool.icon}</span>
<span className="more-card__recent-body">
<strong>{tool.title}</strong>
<small>{categoryLabels[tool.category]}</small>
</span>
</button>
))}
</div>
</section>
)}
{filter === "all" && (
<section className="more-page-v2__section more-page-v2__featured">
<h2 className="more-page-v2__section-title">
<ThunderboltOutlined />
</h2>
<div className="more-page-v2__featured-grid">
{coreTools.map((tool) => (
<button
key={tool.id}
type="button"
className={`more-card more-card--featured${getPreviewClassName(tool.id)}`}
style={{
"--card-gradient": coreToolGradients[tool.id] ?? "linear-gradient(135deg, rgba(var(--accent-rgb), 0.12), rgba(var(--accent-rgb), 0.04))",
} as CSSProperties}
aria-label={`打开核心工具:${tool.title}${tool.text}`}
onClick={() => openTool(tool)}
>
<div className="more-card__featured-body">
<span className="more-card__featured-kicker">{categoryLabels[tool.category]}</span>
<strong>{tool.title}</strong>
<ToolPreviewPanel toolId={tool.id} />
<span className="more-card__featured-desc">{tool.text}</span>
<span className="more-card__steps" aria-hidden="true">
{(coreToolSteps[tool.id] ?? tool.tags.slice(0, 3)).map((step) => (
<span key={step}>{step}</span>
))}
</span>
<span className="more-card__outcome">{tool.useCase}</span>
<span className="more-card__cta">使 </span>
</div>
</button>
))}
</div>
</section>
)}
{(["image", "video"] as ToolCategory[]).map((cat) => {
const group = groupedTools[cat];
if (!group || group.length === 0) return null;
return (
<section key={cat} className="more-page-v2__section">
<h2 className="more-page-v2__section-title">
{categoryIcons[cat]} {categoryLabels[cat]}
<span>{group.length}</span>
</h2>
<div className="more-page-v2__grid">
{group.map((tool) => (
<button
key={tool.id}
type="button"
className={`more-card${tool.ready ? " more-card--ready" : " more-card--pending"}${getPreviewClassName(tool.id)}`}
aria-label={tool.ready ? `打开工具:${tool.title}${tool.text}` : `${tool.title}暂未开放`}
onClick={() => openTool(tool)}
disabled={!tool.ready}
>
<span className="more-card__topline">
{tool.tags.slice(0, 2).map((tag) => (
<span key={tag}>{tag}</span>
))}
</span>
<strong>{tool.title}</strong>
<ToolPreviewPanel toolId={tool.id} />
<span className="more-card__desc">{tool.text}</span>
<span className="more-card__use-case">{tool.useCase}</span>
<span className="more-card__action"> </span>
{tool.badge && <span className="more-card__badge">{tool.badge}</span>}
</button>
))}
</div>
</section>
);
})}
{!hasGroupedTools && (
<section className="more-page-v2__section">
<div className="more-page-v2__empty">
<span className="more-page-v2__empty-icon">
<ClockCircleOutlined />
</span>
<strong>{activeFilterLabel}</strong>
<p>线</p>
<button type="button" className="more-page-v2__empty-action" onClick={() => setFilter("all")}>
</button>
</div>
</section>
)}
</div>
</div>
);
}
export default MorePage;