fix(web): 构建与测试 - 修复会话类型和图标依赖兼容
This commit is contained in:
@@ -1,655 +0,0 @@
|
||||
# 画布列表卡片设计调研报告
|
||||
|
||||
## 一、图片风格分析
|
||||
|
||||
### 核心设计元素识别
|
||||
|
||||
通过对提供的文件夹卡片图片分析,识别出以下关键设计特征:
|
||||
|
||||
#### 1. **玻璃拟态(Glassmorphism)效果**
|
||||
- 半透明背景:使用 `backdrop-filter: blur()` 实现背景模糊
|
||||
- 磨砂玻璃质感:白色半透明叠加层(rgba(255,255,255,0.1) ~ 0.15)
|
||||
- 边框高光:顶部和左侧有微妙的白色边框(1px solid rgba(255,255,255,0.18))
|
||||
|
||||
#### 2. **渐变网格背景(Gradient Mesh)**
|
||||
- 每个卡片顶部有独特的渐变色带(红橙、蓝青、橙红等色系)
|
||||
- 渐变呈现流动的、非线性的网格状分布
|
||||
- 类似 macOS Ventura 的壁纸风格,具有深度和立体感
|
||||
- 渐变从顶部向下过渡到白色/透明
|
||||
|
||||
#### 3. **多层阴影系统**
|
||||
- **主阴影**:较大的扩散阴影 `0 8px 32px rgba(0,0,0,0.12)`
|
||||
- **环境阴影**:模拟物理深度 `0 2px 8px rgba(0,0,0,0.06)`
|
||||
- **内阴影**:卡片内部微妙的阴影增强立体感
|
||||
- 悬停时阴影加深并向上提升:`transform: translateY(-4px)`
|
||||
|
||||
#### 4. **圆角处理**
|
||||
- 大圆角设计:约 `16px - 20px` (r-2xl ~ r-3xl)
|
||||
- 所有元素保持一致的圆角系统
|
||||
- 内部按钮使用更小的圆角(8-12px)
|
||||
|
||||
#### 5. **布局结构**
|
||||
- **三态展示**:
|
||||
1. 正常态(左上):紧凑文件夹样式 + 悬浮按钮
|
||||
2. 堆叠态(右侧):多层卡片堆叠效果,展示深度
|
||||
3. 展开态(左下):完整信息展示 + 时间戳
|
||||
|
||||
#### 6. **交互反馈**
|
||||
- 悬停提升效果(Y轴 -4px)
|
||||
- 鼠标跟随的光泽效果(Glare)
|
||||
- 3D 倾斜动画(轻微的 rotateX/rotateY)
|
||||
- 按钮悬浮显示/隐藏
|
||||
|
||||
---
|
||||
|
||||
## 二、当前项目状态分析
|
||||
|
||||
### 现有组件架构
|
||||
|
||||
#### 1. **CanvasProjectCard** (`canvas-project-card.tsx`)
|
||||
```typescript
|
||||
// 当前结构
|
||||
<article className="app-canvas-project-card">
|
||||
<div className="app-canvas-project-preview">
|
||||
<button className="canvas-project-preview-button">
|
||||
<ProjectPreview /> // 图片或节点预览
|
||||
</button>
|
||||
<span className="canvas-project-select">复选框</span>
|
||||
<div className="canvas-project-cover-meta">节点数量</div>
|
||||
</div>
|
||||
<div className="app-canvas-project-body">
|
||||
<h2>标题</h2>
|
||||
<div className="canvas-project-stats">时间/类型</div>
|
||||
</div>
|
||||
</article>
|
||||
```
|
||||
|
||||
**当前特点**:
|
||||
- 扁平化设计,缺少层次感
|
||||
- 简单的边框和阴影
|
||||
- 预览区域占比大(aspect-video)
|
||||
- 无玻璃拟态效果
|
||||
- 无渐变背景装饰
|
||||
|
||||
#### 2. **CometCard** (`comet-card.tsx`)
|
||||
**已有能力**:
|
||||
- ✅ 3D 倾斜动画(rotateX/rotateY)
|
||||
- ✅ 鼠标跟随效果
|
||||
- ✅ 光泽层(glare)
|
||||
- ✅ 悬停提升动画
|
||||
- ✅ 平滑的弹簧动画
|
||||
|
||||
**缺少**:
|
||||
- ❌ 玻璃拟态背景
|
||||
- ❌ 渐变网格装饰
|
||||
- ❌ 多层阴影系统
|
||||
|
||||
---
|
||||
|
||||
## 三、Aceternity UI 调研
|
||||
|
||||
### 相关组件清单
|
||||
|
||||
#### 1. **3D Card Effect**
|
||||
- 组件路径:`/components/3d-card-effect`
|
||||
- 功能:鼠标跟随的3D倾斜效果
|
||||
- 与 CometCard 功能重叠,我们已有类似实现
|
||||
|
||||
#### 2. **Card Hover Effect**
|
||||
- 组件路径:`/components/card-hover-effect`
|
||||
- 功能:悬停时的边框光晕和背景渐变
|
||||
- 可借鉴:边框渐变的实现方式
|
||||
|
||||
#### 3. **Background Gradient**
|
||||
- 组件路径:`/components/background-gradient`
|
||||
- 功能:动态渐变背景
|
||||
- **重要**:可用于卡片顶部的渐变网格效果
|
||||
|
||||
#### 4. **Meteors**
|
||||
- 组件路径:`/components/meteors`
|
||||
- 功能:流星划过的动画效果
|
||||
- 可选:为卡片增加动态装饰
|
||||
|
||||
#### 5. **Glass Morphism**
|
||||
- 虽然 Aceternity 没有专门的玻璃拟态组件
|
||||
- 但在多个卡片组件中使用了 `backdrop-blur` 和半透明背景
|
||||
- 需要自行实现
|
||||
|
||||
---
|
||||
|
||||
## 四、设计方案
|
||||
|
||||
### 方案 A:渐进式升级(推荐)
|
||||
|
||||
**目标**:在现有 CanvasProjectCard 基础上,逐步添加玻璃拟态和渐变效果。
|
||||
|
||||
#### 4.1 布局设计
|
||||
|
||||
**网格布局**:
|
||||
```css
|
||||
/* 响应式网格 */
|
||||
.canvas-library-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: var(--space-6); /* 24px */
|
||||
padding: var(--space-6);
|
||||
}
|
||||
|
||||
/* 断点优化 */
|
||||
@media (min-width: 640px) {
|
||||
/* 2列:640px - 1024px */
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
/* 3列:1024px - 1440px */
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
|
||||
@media (min-width: 1440px) {
|
||||
/* 4列:>1440px */
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
}
|
||||
```
|
||||
|
||||
**每行显示数量**:
|
||||
- 移动端(<640px):1列(自适应宽度)
|
||||
- 平板(640-1024px):2列
|
||||
- 桌面(1024-1440px):3列
|
||||
- 大屏(>1440px):4列
|
||||
|
||||
#### 4.2 卡片尺寸
|
||||
|
||||
```typescript
|
||||
// 卡片结构
|
||||
<article className="canvas-card-folder">
|
||||
// 宽高比:16:10 (比现有的16:9更接近文件夹比例)
|
||||
// 最小宽度:280px
|
||||
// 最大宽度:360px(防止在大屏上过度拉伸)
|
||||
</article>
|
||||
```
|
||||
|
||||
**尺寸规范**:
|
||||
- **宽度**:280-360px(由网格自动控制)
|
||||
- **高度**:自适应(由内容决定)
|
||||
- **预览区高度**:aspect-[16/10] (~175-225px)
|
||||
- **内容区高度**:~80px(标题 + 元信息)
|
||||
|
||||
#### 4.3 视觉效果分层
|
||||
|
||||
```
|
||||
[层级从底到顶]
|
||||
├─ 1. 基础背景(白色/暗色,根据主题)
|
||||
├─ 2. 渐变网格层(顶部装饰)
|
||||
├─ 3. 玻璃拟态层(backdrop-blur + 半透明)
|
||||
├─ 4. 内容层(文字、图标、预览)
|
||||
├─ 5. 边框高光层(顶部和左侧)
|
||||
└─ 6. 光泽层(hover时的glare效果)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、CSS 实现方案
|
||||
|
||||
### 5.1 玻璃拟态基础样式
|
||||
|
||||
```css
|
||||
.canvas-card-glass {
|
||||
position: relative;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
backdrop-filter: blur(12px) saturate(180%);
|
||||
border: 1px solid rgba(255, 255, 255, 0.18);
|
||||
border-radius: var(--r-2xl); /* 16px */
|
||||
box-shadow:
|
||||
0 8px 32px rgba(0, 0, 0, 0.12),
|
||||
0 2px 8px rgba(0, 0, 0, 0.06),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
||||
transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1),
|
||||
box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
/* 暗色主题 */
|
||||
.dark .canvas-card-glass {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
box-shadow:
|
||||
0 8px 32px rgba(0, 0, 0, 0.4),
|
||||
0 2px 8px rgba(0, 0, 0, 0.2),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.canvas-card-glass:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow:
|
||||
0 16px 48px rgba(0, 0, 0, 0.18),
|
||||
0 4px 12px rgba(0, 0, 0, 0.1),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
```
|
||||
|
||||
### 5.2 渐变网格装饰
|
||||
|
||||
```css
|
||||
.canvas-card-gradient-mesh {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 120px;
|
||||
border-radius: var(--r-2xl) var(--r-2xl) 0 0;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.canvas-card-gradient-mesh::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background:
|
||||
radial-gradient(circle at 20% 30%, rgba(255, 100, 80, 0.6) 0%, transparent 50%),
|
||||
radial-gradient(circle at 80% 20%, rgba(100, 180, 255, 0.5) 0%, transparent 50%),
|
||||
radial-gradient(circle at 50% 70%, rgba(255, 180, 100, 0.4) 0%, transparent 60%);
|
||||
filter: blur(40px);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.canvas-card-gradient-mesh::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(180deg,
|
||||
rgba(255,255,255,0) 0%,
|
||||
rgba(255,255,255,0.95) 80%
|
||||
);
|
||||
}
|
||||
|
||||
/* 每张卡片随机不同的渐变色 */
|
||||
.canvas-card-gradient-mesh[data-variant="1"]::before {
|
||||
background:
|
||||
radial-gradient(circle at 25% 25%, rgba(255, 50, 100, 0.7) 0%, transparent 50%),
|
||||
radial-gradient(circle at 75% 30%, rgba(80, 120, 255, 0.6) 0%, transparent 50%);
|
||||
}
|
||||
|
||||
.canvas-card-gradient-mesh[data-variant="2"]::before {
|
||||
background:
|
||||
radial-gradient(circle at 30% 20%, rgba(0, 200, 180, 0.6) 0%, transparent 50%),
|
||||
radial-gradient(circle at 70% 40%, rgba(100, 255, 150, 0.5) 0%, transparent 50%);
|
||||
}
|
||||
|
||||
.canvas-card-gradient-mesh[data-variant="3"]::before {
|
||||
background:
|
||||
radial-gradient(circle at 20% 35%, rgba(255, 150, 0, 0.7) 0%, transparent 50%),
|
||||
radial-gradient(circle at 80% 25%, rgba(200, 50, 80, 0.6) 0%, transparent 50%);
|
||||
}
|
||||
```
|
||||
|
||||
### 5.3 完整卡片样式
|
||||
|
||||
```css
|
||||
.canvas-card-folder {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 280px;
|
||||
cursor: pointer;
|
||||
isolation: isolate;
|
||||
}
|
||||
|
||||
.canvas-card-folder-inner {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
backdrop-filter: blur(12px) saturate(180%);
|
||||
border: 1px solid rgba(255, 255, 255, 0.18);
|
||||
border-radius: var(--r-2xl);
|
||||
overflow: hidden;
|
||||
box-shadow:
|
||||
0 8px 32px rgba(0, 0, 0, 0.12),
|
||||
0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
}
|
||||
|
||||
.canvas-card-folder:hover .canvas-card-folder-inner {
|
||||
transform: translateY(-4px) scale(1.01);
|
||||
box-shadow:
|
||||
0 16px 48px rgba(0, 0, 0, 0.18),
|
||||
0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
border-color: rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
|
||||
/* 预览区域 */
|
||||
.canvas-card-preview {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
aspect-ratio: 16 / 10;
|
||||
overflow: hidden;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.canvas-card-preview img,
|
||||
.canvas-card-preview video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.canvas-card-folder:hover .canvas-card-preview img,
|
||||
.canvas-card-folder:hover .canvas-card-preview video {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
/* 内容区域 */
|
||||
.canvas-card-content {
|
||||
position: relative;
|
||||
padding: var(--space-4);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.canvas-card-title {
|
||||
font-size: var(--fs-body);
|
||||
font-weight: 600;
|
||||
line-height: 1.4;
|
||||
color: var(--color-neutral-900);
|
||||
margin-bottom: var(--space-2);
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dark .canvas-card-title {
|
||||
color: var(--color-neutral-50);
|
||||
}
|
||||
|
||||
.canvas-card-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
font-size: var(--fs-caption);
|
||||
color: var(--color-neutral-600);
|
||||
}
|
||||
|
||||
.dark .canvas-card-meta {
|
||||
color: var(--color-neutral-400);
|
||||
}
|
||||
|
||||
/* 悬浮按钮 */
|
||||
.canvas-card-action-button {
|
||||
position: absolute;
|
||||
bottom: var(--space-3);
|
||||
right: var(--space-3);
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
backdrop-filter: blur(8px);
|
||||
border-radius: var(--r-lg);
|
||||
color: white;
|
||||
opacity: 0;
|
||||
transform: translateY(8px);
|
||||
transition: all 0.25s ease;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.canvas-card-folder:hover .canvas-card-action-button {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.canvas-card-action-button:hover {
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 六、组件实现方案
|
||||
|
||||
### 6.1 新建组件:CanvasFolderCard
|
||||
|
||||
```tsx
|
||||
// web/src/components/canvas/canvas-folder-card.tsx
|
||||
|
||||
import { motion } from "motion/react";
|
||||
import { Plus, MoreHorizontal } from "lucide-react";
|
||||
import { CometCard } from "@/components/ui/aceternity/comet-card";
|
||||
import type { CanvasProject } from "@/stores/canvas/use-canvas-store";
|
||||
|
||||
interface CanvasFolderCardProps {
|
||||
project: CanvasProject;
|
||||
variant?: number; // 1-3,决定渐变色方案
|
||||
onClick?: () => void;
|
||||
onAction?: () => void;
|
||||
}
|
||||
|
||||
export function CanvasFolderCard({
|
||||
project,
|
||||
variant = 1,
|
||||
onClick,
|
||||
onAction
|
||||
}: CanvasFolderCardProps) {
|
||||
return (
|
||||
<CometCard
|
||||
className="canvas-card-folder"
|
||||
glare={true}
|
||||
rotateDepth={3}
|
||||
translateDepth={3}
|
||||
onClick={onClick}
|
||||
>
|
||||
<div className="canvas-card-folder-inner">
|
||||
{/* 渐变网格装饰 */}
|
||||
<div
|
||||
className="canvas-card-gradient-mesh"
|
||||
data-variant={variant}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
|
||||
{/* 预览区域 */}
|
||||
<div className="canvas-card-preview">
|
||||
<ProjectPreview project={project} />
|
||||
</div>
|
||||
|
||||
{/* 内容区域 */}
|
||||
<div className="canvas-card-content">
|
||||
<h3 className="canvas-card-title">{project.title}</h3>
|
||||
<div className="canvas-card-meta">
|
||||
<span>{project.nodes.length} 节点</span>
|
||||
<span aria-hidden="true">·</span>
|
||||
<time dateTime={project.updatedAt}>
|
||||
{formatTime(project.updatedAt)}
|
||||
</time>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 悬浮按钮 */}
|
||||
<motion.button
|
||||
className="canvas-card-action-button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onAction?.();
|
||||
}}
|
||||
whileHover={{ scale: 1.1 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
aria-label="更多操作"
|
||||
>
|
||||
<MoreHorizontal className="size-5" />
|
||||
</motion.button>
|
||||
</div>
|
||||
</CometCard>
|
||||
);
|
||||
}
|
||||
|
||||
// 预览组件保持原有逻辑
|
||||
function ProjectPreview({ project }: { project: CanvasProject }) {
|
||||
// ... 复用现有的 ProjectPreview 逻辑
|
||||
}
|
||||
|
||||
function formatTime(value: string) {
|
||||
// ... 复用现有的时间格式化逻辑
|
||||
}
|
||||
```
|
||||
|
||||
### 6.2 集成到现有页面
|
||||
|
||||
```tsx
|
||||
// 在 pages/home/index.tsx 或 pages/canvas/index.tsx 中
|
||||
|
||||
import { CanvasFolderCard } from "@/components/canvas/canvas-folder-card";
|
||||
|
||||
// 替换现有的 CanvasProjectCard
|
||||
<div className="canvas-library-grid">
|
||||
{canvasProjects.map((project, index) => (
|
||||
<CanvasFolderCard
|
||||
key={project.id}
|
||||
project={project}
|
||||
variant={(index % 3) + 1} // 循环使用3种渐变方案
|
||||
onClick={() => navigate(`/canvas/${project.id}`)}
|
||||
onAction={() => handleMore(project)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 七、技术要点
|
||||
|
||||
### 7.1 性能优化
|
||||
|
||||
```typescript
|
||||
// 1. 渐变色方案预计算
|
||||
const GRADIENT_VARIANTS = [
|
||||
{ id: 1, colors: ['rgba(255,50,100,0.7)', 'rgba(80,120,255,0.6)'] },
|
||||
{ id: 2, colors: ['rgba(0,200,180,0.6)', 'rgba(100,255,150,0.5)'] },
|
||||
{ id: 3, colors: ['rgba(255,150,0,0.7)', 'rgba(200,50,80,0.6)'] },
|
||||
];
|
||||
|
||||
// 2. 使用 CSS 变量动态注入
|
||||
<div
|
||||
style={{
|
||||
'--gradient-color-1': GRADIENT_VARIANTS[variant].colors[0],
|
||||
'--gradient-color-2': GRADIENT_VARIANTS[variant].colors[1],
|
||||
}}
|
||||
/>
|
||||
|
||||
// 3. backdrop-filter 回退方案
|
||||
@supports not (backdrop-filter: blur(12px)) {
|
||||
.canvas-card-folder-inner {
|
||||
background: rgba(255, 255, 255, 0.95); /* 完全不透明 */
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 7.2 无障碍支持
|
||||
|
||||
```tsx
|
||||
// 完整的 ARIA 标签
|
||||
<article
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label={`画布:${project.title},${project.nodes.length}个节点`}
|
||||
onClick={onClick}
|
||||
onKeyDown={(e) => e.key === 'Enter' && onClick?.()}
|
||||
>
|
||||
{/* ... */}
|
||||
</article>
|
||||
```
|
||||
|
||||
### 7.3 暗色主题适配
|
||||
|
||||
```css
|
||||
/* 自动适配当前主题 */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.canvas-card-folder-inner {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-color: rgba(255, 255, 255, 0.12);
|
||||
}
|
||||
|
||||
.canvas-card-gradient-mesh::after {
|
||||
background: linear-gradient(180deg,
|
||||
rgba(10, 10, 15, 0) 0%,
|
||||
rgba(10, 10, 15, 0.95) 80%
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 八、实施建议
|
||||
|
||||
### 阶段 1:基础重构(1-2天)
|
||||
1. ✅ 创建 `CanvasFolderCard` 组件
|
||||
2. ✅ 实现玻璃拟态样式
|
||||
3. ✅ 集成 `CometCard` 实现3D效果
|
||||
4. ✅ 调整网格布局为新的响应式方案
|
||||
|
||||
### 阶段 2:视觉增强(2-3天)
|
||||
1. ✅ 添加渐变网格装饰层
|
||||
2. ✅ 实现多层阴影系统
|
||||
3. ✅ 优化悬停动画和过渡
|
||||
4. ✅ 添加光泽层(glare effect)
|
||||
|
||||
### 阶段 3:细节打磨(1-2天)
|
||||
1. ✅ 暗色主题适配
|
||||
2. ✅ 无障碍测试和优化
|
||||
3. ✅ 性能测试(backdrop-filter 的性能影响)
|
||||
4. ✅ 跨浏览器兼容性测试
|
||||
|
||||
### 阶段 4:用户测试(1天)
|
||||
1. ✅ A/B 测试(新旧卡片对比)
|
||||
2. ✅ 收集用户反馈
|
||||
3. ✅ 根据反馈微调
|
||||
|
||||
---
|
||||
|
||||
## 九、风险和注意事项
|
||||
|
||||
### 9.1 性能风险
|
||||
- **backdrop-filter 性能消耗**:在大量卡片(50+)时可能影响滚动性能
|
||||
- 解决方案:使用虚拟滚动或懒加载
|
||||
- 回退方案:低性能设备禁用 backdrop-filter
|
||||
|
||||
### 9.2 兼容性
|
||||
- **Safari < 15**:backdrop-filter 支持不完整
|
||||
- 解决方案:检测支持度,提供回退样式
|
||||
|
||||
### 9.3 可访问性
|
||||
- 确保玻璃拟态效果不影响文字对比度(WCAG AA 标准:4.5:1)
|
||||
- 光泽效果不应干扰屏幕阅读器
|
||||
|
||||
---
|
||||
|
||||
## 十、参考资源
|
||||
|
||||
### 设计灵感
|
||||
- macOS Ventura 文件夹设计
|
||||
- iOS 16+ 小组件玻璃拟态
|
||||
- Figma Community: Glassmorphism UI Kit
|
||||
|
||||
### 技术文档
|
||||
- [MDN: backdrop-filter](https://developer.mozilla.org/en-US/docs/Web/CSS/backdrop-filter)
|
||||
- [CSS Tricks: Glassmorphism](https://css-tricks.com/glassmorphism/)
|
||||
- [Aceternity UI Components](https://ui.aceternity.com/components)
|
||||
|
||||
### 现有代码
|
||||
- `/web/src/components/canvas/canvas-project-card.tsx`
|
||||
- `/web/src/components/ui/aceternity/comet-card.tsx`
|
||||
- `/web/src/styles/globals.css` (token system)
|
||||
|
||||
---
|
||||
|
||||
## 总结
|
||||
|
||||
这套设计方案结合了:
|
||||
1. **玻璃拟态**的现代感和层次感
|
||||
2. **渐变网格**的视觉吸引力和品牌差异化
|
||||
3. **3D交互**的沉浸感(已有CometCard支持)
|
||||
4. **响应式布局**的灵活性(1-4列自适应)
|
||||
|
||||
建议采用**渐进式升级**策略,先实现核心玻璃拟态效果,再逐步添加渐变装饰和高级交互,确保每个阶段都能交付可用版本。
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,7 @@ import type { MouseEvent as ReactMouseEvent } from "react";
|
||||
|
||||
import { canvasThemes } from "@/lib/canvas-theme";
|
||||
import { useThemeStore } from "@/stores/use-theme-store";
|
||||
import { STORYBOARD_HEADER_HEIGHT, STORYBOARD_ROW_HEIGHT, storyboardTableHeight } from "@/components/canvas/canvas-script-node";
|
||||
import { STORYBOARD_HEADER_HEIGHT, STORYBOARD_ROW_HEIGHT, storyboardTableHeight } from "@/lib/canvas/canvas-storyboard-layout";
|
||||
import type { CanvasConnection, CanvasNodeData, ConnectionHandle, Position } from "@/types/canvas";
|
||||
|
||||
export const ConnectionPath = React.memo(function ConnectionPath({
|
||||
|
||||
@@ -3,9 +3,9 @@ import type { ReactNode } from "react";
|
||||
import { AlertCircle, BookOpenCheck, CheckCircle2, ChevronRight, Clapperboard, Image as ImageIcon, Lock, Maximize2, Music2, Pencil, Plus, Replace, Settings2, Star, Type, Upload, Video } from "lucide-react";
|
||||
|
||||
import { canvasThemes } from "@/lib/canvas-theme";
|
||||
import { storyboardMinNodeHeight } from "@/lib/canvas/canvas-storyboard-layout";
|
||||
import { resourceStorageLabel, resourceStorageLocation, resourceStorageTitle } from "@/lib/canvas/resource-storage-status";
|
||||
import { useThemeStore } from "@/stores/use-theme-store";
|
||||
import { storyboardMinNodeHeight } from "./canvas-script-node";
|
||||
import { CanvasNodeType, type CanvasNodeData, type Position } from "@/types/canvas";
|
||||
import type { CanvasResourceReference } from "@/lib/canvas/canvas-resource-references";
|
||||
import { MEDIA_NODE_MIN_SIZE } from "@/lib/canvas/canvas-node-size";
|
||||
|
||||
@@ -15,6 +15,7 @@ import { navigateToSettings } from "@/lib/settings-navigation";
|
||||
import { canvasThemes } from "@/lib/canvas-theme";
|
||||
import { useEffectiveConfig } from "@/stores/use-config-store";
|
||||
import { useThemeStore } from "@/stores/use-theme-store";
|
||||
import { STORYBOARD_COMPOSER_MIN_HEIGHT, STORYBOARD_HEADER_HEIGHT, STORYBOARD_ROW_HEIGHT, storyboardTableHeight } from "@/lib/canvas/canvas-storyboard-layout";
|
||||
import type {
|
||||
CanvasGenerationBatch,
|
||||
CanvasGenerationBatchItem,
|
||||
@@ -30,29 +31,11 @@ import type {
|
||||
} from "@/types/canvas";
|
||||
import type { TaskStatus } from "@/services/api/task-center";
|
||||
|
||||
export const STORYBOARD_ROW_HEIGHT = 48;
|
||||
export const STORYBOARD_HEADER_HEIGHT = 124;
|
||||
const STORYBOARD_ADD_ROW_HEIGHT = 36;
|
||||
const STORYBOARD_COMPOSER_MIN_HEIGHT = 104;
|
||||
const STORYBOARD_COMPOSER_MAX_HEIGHT = 180;
|
||||
const STORYBOARD_PROMPT_MIN_HEIGHT = 40;
|
||||
const STORYBOARD_PROMPT_MAX_HEIGHT = 116;
|
||||
const SCRIPT_GRID_TEMPLATE = "72px 150px minmax(280px, 1.4fr) minmax(220px, 1fr) 58px";
|
||||
const EMPTY_STORYBOARD_ROWS: StoryboardRow[] = [];
|
||||
|
||||
export function storyboardNodeHeight(rowCount: number, composerHeight = STORYBOARD_COMPOSER_MIN_HEIGHT) {
|
||||
const visibleRows = Math.min(Math.max(rowCount, 1), 4);
|
||||
return STORYBOARD_HEADER_HEIGHT + visibleRows * STORYBOARD_ROW_HEIGHT + STORYBOARD_ADD_ROW_HEIGHT + Math.min(STORYBOARD_COMPOSER_MAX_HEIGHT, Math.max(STORYBOARD_COMPOSER_MIN_HEIGHT, composerHeight));
|
||||
}
|
||||
|
||||
export function storyboardMinNodeHeight(composerHeight = STORYBOARD_COMPOSER_MIN_HEIGHT) {
|
||||
return STORYBOARD_HEADER_HEIGHT + STORYBOARD_ROW_HEIGHT + STORYBOARD_ADD_ROW_HEIGHT + Math.min(STORYBOARD_COMPOSER_MAX_HEIGHT, Math.max(STORYBOARD_COMPOSER_MIN_HEIGHT, composerHeight));
|
||||
}
|
||||
|
||||
export function storyboardTableHeight(nodeHeight: number, composerHeight = STORYBOARD_COMPOSER_MIN_HEIGHT) {
|
||||
return Math.max(STORYBOARD_ROW_HEIGHT, nodeHeight - STORYBOARD_HEADER_HEIGHT - STORYBOARD_ADD_ROW_HEIGHT - Math.min(STORYBOARD_COMPOSER_MAX_HEIGHT, Math.max(STORYBOARD_COMPOSER_MIN_HEIGHT, composerHeight)));
|
||||
}
|
||||
|
||||
const columnOptions: Array<{ label: string; value: StoryboardColumn }> = [
|
||||
{ label: "序号", value: "shotNumber" },
|
||||
{ label: "时长", value: "durationSeconds" },
|
||||
|
||||
@@ -37,7 +37,7 @@ export function WorkspaceSidebarFooter({ expandedClassName, collapsedClassName,
|
||||
const handleLogout = async () => {
|
||||
try {
|
||||
await logout();
|
||||
await applyUserSession({ user: null, systemChannels: [] });
|
||||
await applyUserSession({ user: null });
|
||||
setMenuOpen(false);
|
||||
message.success("已退出登录");
|
||||
navigate("/login", { replace: true });
|
||||
|
||||
@@ -2,14 +2,21 @@ import { useMemo, useState, type ComponentType, type SVGProps } from "react";
|
||||
import { Input, Popover } from "antd";
|
||||
import { Cpu, Search, X } from "lucide-react";
|
||||
|
||||
import * as LobeIcons from "@lobehub/icons/es/icons";
|
||||
import { toc } from "@lobehub/icons/es/toc";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type LobeIconComponent = ComponentType<SVGProps<SVGSVGElement> & { size?: number | string }>;
|
||||
|
||||
const iconRegistry = LobeIcons as unknown as Record<string, LobeIconComponent>;
|
||||
// 只加载各图标的 Mono 组件;完整 icons barrel 会经由 Avatar/Combine 与 @lobehub/ui 形成循环依赖,Bun 测试环境会触发未初始化导出。
|
||||
const iconModules = typeof import.meta.glob === "function"
|
||||
? import.meta.glob("../../node_modules/@lobehub/icons/es/*/components/Mono.js", { eager: true, import: "default" })
|
||||
: {};
|
||||
const iconRegistry = Object.fromEntries(
|
||||
Object.entries(iconModules)
|
||||
.map(([path, icon]) => [path.match(/\/([^/]+)\/components\/Mono\.js$/)?.[1], icon])
|
||||
.filter((entry): entry is [string, LobeIconComponent] => Boolean(entry[0] && entry[1])),
|
||||
) as Record<string, LobeIconComponent>;
|
||||
const iconOptions = toc
|
||||
.filter((item) => item.group === "model" || item.group === "provider")
|
||||
.map((item) => ({ id: item.id, title: item.fullTitle || item.title }))
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { NODE_DEFAULT_SIZE, getNodeSpec } from "@/constant/canvas";
|
||||
import { STORYBOARD_HEADER_HEIGHT, STORYBOARD_ROW_HEIGHT, storyboardTableHeight } from "@/components/canvas/canvas-script-node";
|
||||
import { STORYBOARD_HEADER_HEIGHT, STORYBOARD_ROW_HEIGHT, storyboardTableHeight } from "@/lib/canvas/canvas-storyboard-layout";
|
||||
import type { CanvasImageAngleParams } from "@/components/canvas/canvas-node-angle-dialog";
|
||||
import type { NodeGenerationInput } from "@/components/canvas/canvas-node-generation";
|
||||
import { isFrameNode } from "@/lib/canvas/canvas-frame";
|
||||
|
||||
24
web/src/lib/canvas/canvas-storyboard-layout.ts
Normal file
24
web/src/lib/canvas/canvas-storyboard-layout.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
export const STORYBOARD_ROW_HEIGHT = 48;
|
||||
export const STORYBOARD_HEADER_HEIGHT = 124;
|
||||
|
||||
const STORYBOARD_ADD_ROW_HEIGHT = 36;
|
||||
export const STORYBOARD_COMPOSER_MIN_HEIGHT = 104;
|
||||
const STORYBOARD_COMPOSER_MAX_HEIGHT = 180;
|
||||
|
||||
// 布局计算属于画布领域逻辑,独立于 React 节点组件,避免纯函数测试加载完整 UI 依赖。
|
||||
function normalizedComposerHeight(composerHeight: number) {
|
||||
return Math.min(STORYBOARD_COMPOSER_MAX_HEIGHT, Math.max(STORYBOARD_COMPOSER_MIN_HEIGHT, composerHeight));
|
||||
}
|
||||
|
||||
export function storyboardNodeHeight(rowCount: number, composerHeight = STORYBOARD_COMPOSER_MIN_HEIGHT) {
|
||||
const visibleRows = Math.min(Math.max(rowCount, 1), 4);
|
||||
return STORYBOARD_HEADER_HEIGHT + visibleRows * STORYBOARD_ROW_HEIGHT + STORYBOARD_ADD_ROW_HEIGHT + normalizedComposerHeight(composerHeight);
|
||||
}
|
||||
|
||||
export function storyboardMinNodeHeight(composerHeight = STORYBOARD_COMPOSER_MIN_HEIGHT) {
|
||||
return STORYBOARD_HEADER_HEIGHT + STORYBOARD_ROW_HEIGHT + STORYBOARD_ADD_ROW_HEIGHT + normalizedComposerHeight(composerHeight);
|
||||
}
|
||||
|
||||
export function storyboardTableHeight(nodeHeight: number, composerHeight = STORYBOARD_COMPOSER_MIN_HEIGHT) {
|
||||
return Math.max(STORYBOARD_ROW_HEIGHT, nodeHeight - STORYBOARD_HEADER_HEIGHT - STORYBOARD_ADD_ROW_HEIGHT - normalizedComposerHeight(composerHeight));
|
||||
}
|
||||
@@ -54,7 +54,8 @@ import { AssetPickerModal, type InsertAssetPayload } from "@/components/canvas/a
|
||||
import { getProject } from "@/services/api/projects";
|
||||
import { CanvasZoomControls } from "@/components/canvas/canvas-zoom-controls";
|
||||
import { CanvasShareModal } from "@/components/canvas/canvas-share-modal";
|
||||
import { CanvasScriptEditor, CanvasScriptNodeContent, STORYBOARD_HEADER_HEIGHT, STORYBOARD_ROW_HEIGHT, storyboardMinNodeHeight, storyboardTableHeight } from "@/components/canvas/canvas-script-node";
|
||||
import { CanvasScriptEditor, CanvasScriptNodeContent } from "@/components/canvas/canvas-script-node";
|
||||
import { STORYBOARD_HEADER_HEIGHT, STORYBOARD_ROW_HEIGHT, storyboardMinNodeHeight, storyboardTableHeight } from "@/lib/canvas/canvas-storyboard-layout";
|
||||
import { CanvasDirectorNodePanel } from "@/components/canvas/director/canvas-director-node-panel";
|
||||
import { CanvasVersionCompareModal } from "@/components/canvas/canvas-version-compare-modal";
|
||||
import { CanvasLocalAgentPanel } from "@/components/canvas/canvas-local-agent-panel";
|
||||
|
||||
Reference in New Issue
Block a user