Files
ttstd-infinite-canvas/design-research-folder-card.md

17 KiB
Raw Blame History

画布列表卡片设计调研报告

一、图片风格分析

核心设计元素识别

通过对提供的文件夹卡片图片分析,识别出以下关键设计特征:

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)

// 当前结构
<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 布局设计

网格布局:

/* 响应式网格 */
.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 卡片尺寸

// 卡片结构
<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 玻璃拟态基础样式

.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 渐变网格装饰

.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 完整卡片样式

.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

// 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 集成到现有页面

// 在 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 性能优化

// 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 无障碍支持

// 完整的 ARIA 标签
<article 
  role="button"
  tabIndex={0}
  aria-label={`画布:${project.title},${project.nodes.length}个节点`}
  onClick={onClick}
  onKeyDown={(e) => e.key === 'Enter' && onClick?.()}
>
  {/* ... */}
</article>

7.3 暗色主题适配

/* 自动适配当前主题 */
@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

技术文档

现有代码

  • /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列自适应)

建议采用渐进式升级策略,先实现核心玻璃拟态效果,再逐步添加渐变装饰和高级交互,确保每个阶段都能交付可用版本。