696 lines
15 KiB
Markdown
696 lines
15 KiB
Markdown
---
|
||
name: frontend-design
|
||
description: Create distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
|
||
arguments: [component|page|storybook] <description>
|
||
---
|
||
|
||
# Frontend Design 前端设计技能
|
||
|
||
创建高质量、有设计感的前端界面和组件,支持 Storybook 组件开发。
|
||
|
||
---
|
||
|
||
## 命令格式
|
||
|
||
| 命令 | 功能 | 示例 |
|
||
|------|------|------|
|
||
| `/frontend-design component <描述>` | 创建 React/Vue 组件 | `/frontend-design component 产品卡片` |
|
||
| `/frontend-design page <描述>` | 创建完整页面 | `/frontend-design page 登录页` |
|
||
| `/frontend-design storybook <描述>` | 创建带 Storybook 的组件 | `/frontend-design storybook 按钮组件` |
|
||
|
||
---
|
||
|
||
## 设计原则
|
||
|
||
### 1. 设计思维先行
|
||
|
||
在编码前,明确以下问题:
|
||
|
||
- **目的**:这个界面解决什么问题?谁在使用?
|
||
- **调性**:选择一个明确的美学方向
|
||
- **差异化**:什么让这个设计令人难忘?
|
||
|
||
### 2. 美学方向选择
|
||
|
||
| 风格 | 特点 | 适用场景 |
|
||
|------|------|----------|
|
||
| 极简主义 | 大量留白、精炼元素 | 工具类、专业平台 |
|
||
| 现代商务 | 清晰层次、专业配色 | 企业官网、B2B |
|
||
| 活力年轻 | 鲜艳色彩、动感动画 | 消费品、社交 |
|
||
| 奢华精致 | 深色调、金属质感 | 高端品牌、金融 |
|
||
| 自然有机 | 柔和曲线、自然色系 | 健康、环保 |
|
||
| 复古怀旧 | 经典字体、做旧质感 | 文化、艺术 |
|
||
| 未来科技 | 渐变、玻璃拟态 | 科技、创新 |
|
||
|
||
### 3. 避免的设计陷阱
|
||
|
||
**禁止使用**:
|
||
- 过度使用的字体:Inter、Roboto、Arial
|
||
- 陈词滥调的配色:紫色渐变白底
|
||
- 千篇一律的布局
|
||
- 缺乏个性的通用组件
|
||
|
||
**应该追求**:
|
||
- 独特的字体组合
|
||
- 有意图的配色方案
|
||
- 打破常规的布局
|
||
- 有记忆点的细节
|
||
|
||
---
|
||
|
||
## Storybook 组件开发
|
||
|
||
### 项目结构
|
||
|
||
```
|
||
src/
|
||
├── components/
|
||
│ ├── Button/
|
||
│ │ ├── Button.tsx
|
||
│ │ ├── Button.stories.tsx
|
||
│ │ ├── Button.module.css
|
||
│ │ └── index.ts
|
||
│ ├── Card/
|
||
│ │ ├── Card.tsx
|
||
│ │ ├── Card.stories.tsx
|
||
│ │ ├── Card.module.css
|
||
│ │ └── index.ts
|
||
│ └── index.ts
|
||
├── styles/
|
||
│ ├── variables.css
|
||
│ ├── typography.css
|
||
│ └── animations.css
|
||
└── .storybook/
|
||
├── main.ts
|
||
└── preview.ts
|
||
```
|
||
|
||
### 组件模板
|
||
|
||
#### 1. 组件文件 (Component.tsx)
|
||
|
||
```tsx
|
||
import React from 'react';
|
||
import styles from './Component.module.css';
|
||
|
||
export interface ComponentProps {
|
||
/** 组件变体 */
|
||
variant?: 'primary' | 'secondary' | 'outline';
|
||
/** 尺寸 */
|
||
size?: 'sm' | 'md' | 'lg';
|
||
/** 是否禁用 */
|
||
disabled?: boolean;
|
||
/** 子元素 */
|
||
children: React.ReactNode;
|
||
/** 点击事件 */
|
||
onClick?: () => void;
|
||
}
|
||
|
||
export const Component: React.FC<ComponentProps> = ({
|
||
variant = 'primary',
|
||
size = 'md',
|
||
disabled = false,
|
||
children,
|
||
onClick,
|
||
}) => {
|
||
return (
|
||
<div
|
||
className={`${styles.component} ${styles[variant]} ${styles[size]}`}
|
||
data-disabled={disabled}
|
||
onClick={disabled ? undefined : onClick}
|
||
>
|
||
{children}
|
||
</div>
|
||
);
|
||
};
|
||
```
|
||
|
||
#### 2. Storybook Stories (Component.stories.tsx)
|
||
|
||
```tsx
|
||
import type { Meta, StoryObj } from '@storybook/react';
|
||
import { Component } from './Component';
|
||
|
||
const meta: Meta<typeof Component> = {
|
||
title: 'Components/Component',
|
||
component: Component,
|
||
tags: ['autodocs'],
|
||
parameters: {
|
||
layout: 'centered',
|
||
docs: {
|
||
description: {
|
||
component: '组件描述文档',
|
||
},
|
||
},
|
||
},
|
||
argTypes: {
|
||
variant: {
|
||
control: 'select',
|
||
options: ['primary', 'secondary', 'outline'],
|
||
description: '组件变体样式',
|
||
},
|
||
size: {
|
||
control: 'radio',
|
||
options: ['sm', 'md', 'lg'],
|
||
description: '组件尺寸',
|
||
},
|
||
disabled: {
|
||
control: 'boolean',
|
||
description: '是否禁用',
|
||
},
|
||
},
|
||
};
|
||
|
||
export default meta;
|
||
type Story = StoryObj<typeof Component>;
|
||
|
||
/** 默认状态 */
|
||
export const Default: Story = {
|
||
args: {
|
||
children: '默认组件',
|
||
},
|
||
};
|
||
|
||
/** 主要变体 */
|
||
export const Primary: Story = {
|
||
args: {
|
||
variant: 'primary',
|
||
children: '主要按钮',
|
||
},
|
||
};
|
||
|
||
/** 次要变体 */
|
||
export const Secondary: Story = {
|
||
args: {
|
||
variant: 'secondary',
|
||
children: '次要按钮',
|
||
},
|
||
};
|
||
|
||
/** 不同尺寸 */
|
||
export const Sizes: Story = {
|
||
render: () => (
|
||
<div style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
|
||
<Component size="sm">小号</Component>
|
||
<Component size="md">中号</Component>
|
||
<Component size="lg">大号</Component>
|
||
</div>
|
||
),
|
||
};
|
||
|
||
/** 禁用状态 */
|
||
export const Disabled: Story = {
|
||
args: {
|
||
disabled: true,
|
||
children: '禁用状态',
|
||
},
|
||
};
|
||
```
|
||
|
||
#### 3. 样式文件 (Component.module.css)
|
||
|
||
```css
|
||
.component {
|
||
/* 基础样式 */
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border-radius: var(--radius-md);
|
||
font-family: var(--font-sans);
|
||
font-weight: 500;
|
||
cursor: pointer;
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
/* 变体 */
|
||
.primary {
|
||
background: var(--color-primary);
|
||
color: white;
|
||
}
|
||
|
||
.primary:hover {
|
||
background: var(--color-primary-dark);
|
||
transform: translateY(-1px);
|
||
box-shadow: 0 4px 12px var(--color-primary-shadow);
|
||
}
|
||
|
||
.secondary {
|
||
background: var(--color-secondary);
|
||
color: var(--color-text);
|
||
}
|
||
|
||
.outline {
|
||
background: transparent;
|
||
border: 2px solid var(--color-border);
|
||
color: var(--color-text);
|
||
}
|
||
|
||
/* 尺寸 */
|
||
.sm {
|
||
padding: 0.5rem 1rem;
|
||
font-size: 0.875rem;
|
||
}
|
||
|
||
.md {
|
||
padding: 0.75rem 1.5rem;
|
||
font-size: 1rem;
|
||
}
|
||
|
||
.lg {
|
||
padding: 1rem 2rem;
|
||
font-size: 1.125rem;
|
||
}
|
||
|
||
/* 状态 */
|
||
[data-disabled="true"] {
|
||
opacity: 0.5;
|
||
cursor: not-allowed;
|
||
pointer-events: none;
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 设计系统变量
|
||
|
||
### CSS 变量模板
|
||
|
||
```css
|
||
:root {
|
||
/* 颜色 */
|
||
--color-primary: #0066ff;
|
||
--color-primary-dark: #0052cc;
|
||
--color-primary-light: #4d94ff;
|
||
--color-primary-shadow: rgba(0, 102, 255, 0.25);
|
||
|
||
--color-secondary: #f0f4f8;
|
||
--color-accent: #ff6b35;
|
||
|
||
--color-text: #1a1a2e;
|
||
--color-text-muted: #64748b;
|
||
--color-text-inverse: #ffffff;
|
||
|
||
--color-background: #ffffff;
|
||
--color-surface: #f8fafc;
|
||
--color-border: #e2e8f0;
|
||
|
||
--color-success: #10b981;
|
||
--color-warning: #f59e0b;
|
||
--color-error: #ef4444;
|
||
|
||
/* 字体 */
|
||
--font-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
|
||
--font-display: 'Clash Display', var(--font-sans);
|
||
--font-mono: 'JetBrains Mono', monospace;
|
||
|
||
/* 字号 */
|
||
--text-xs: 0.75rem;
|
||
--text-sm: 0.875rem;
|
||
--text-base: 1rem;
|
||
--text-lg: 1.125rem;
|
||
--text-xl: 1.25rem;
|
||
--text-2xl: 1.5rem;
|
||
--text-3xl: 2rem;
|
||
--text-4xl: 2.5rem;
|
||
|
||
/* 间距 */
|
||
--space-1: 0.25rem;
|
||
--space-2: 0.5rem;
|
||
--space-3: 0.75rem;
|
||
--space-4: 1rem;
|
||
--space-6: 1.5rem;
|
||
--space-8: 2rem;
|
||
--space-12: 3rem;
|
||
--space-16: 4rem;
|
||
|
||
/* 圆角 */
|
||
--radius-sm: 0.25rem;
|
||
--radius-md: 0.5rem;
|
||
--radius-lg: 1rem;
|
||
--radius-xl: 1.5rem;
|
||
--radius-full: 9999px;
|
||
|
||
/* 阴影 */
|
||
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
|
||
--shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
|
||
|
||
/* 动画 */
|
||
--duration-fast: 150ms;
|
||
--duration-normal: 300ms;
|
||
--duration-slow: 500ms;
|
||
--ease-out: cubic-bezier(0.16, 1, 0.3, 1);
|
||
--ease-bounce: cubic-bezier(0.34, 1.56, 0.64, 1);
|
||
}
|
||
|
||
/* 暗色主题 */
|
||
[data-theme="dark"] {
|
||
--color-text: #f1f5f9;
|
||
--color-text-muted: #94a3b8;
|
||
--color-background: #0f172a;
|
||
--color-surface: #1e293b;
|
||
--color-border: #334155;
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 常用组件示例
|
||
|
||
### 1. 产品卡片 (ProductCard)
|
||
|
||
```tsx
|
||
// ProductCard.tsx
|
||
import React from 'react';
|
||
import styles from './ProductCard.module.css';
|
||
|
||
export interface ProductCardProps {
|
||
image: string;
|
||
title: string;
|
||
location: string;
|
||
rating: number;
|
||
reviewCount: number;
|
||
price: number;
|
||
originalPrice?: number;
|
||
tags?: string[];
|
||
onAddToCart?: () => void;
|
||
}
|
||
|
||
export const ProductCard: React.FC<ProductCardProps> = ({
|
||
image,
|
||
title,
|
||
location,
|
||
rating,
|
||
reviewCount,
|
||
price,
|
||
originalPrice,
|
||
tags = [],
|
||
onAddToCart,
|
||
}) => {
|
||
return (
|
||
<article className={styles.card}>
|
||
<div className={styles.imageWrapper}>
|
||
<img src={image} alt={title} className={styles.image} />
|
||
{tags.length > 0 && (
|
||
<div className={styles.tags}>
|
||
{tags.map((tag) => (
|
||
<span key={tag} className={styles.tag} data-tag={tag}>
|
||
{tag}
|
||
</span>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
<div className={styles.content}>
|
||
<h3 className={styles.title}>{title}</h3>
|
||
<p className={styles.location}>📍 {location}</p>
|
||
|
||
<div className={styles.rating}>
|
||
<span className={styles.stars}>⭐ {rating.toFixed(1)}</span>
|
||
<span className={styles.reviewCount}>({reviewCount}条评价)</span>
|
||
</div>
|
||
|
||
<div className={styles.priceRow}>
|
||
<div className={styles.price}>
|
||
<span className={styles.currency}>¥</span>
|
||
<span className={styles.amount}>{price}</span>
|
||
<span className={styles.suffix}>起</span>
|
||
</div>
|
||
{originalPrice && (
|
||
<span className={styles.originalPrice}>¥{originalPrice}</span>
|
||
)}
|
||
</div>
|
||
|
||
<button className={styles.addButton} onClick={onAddToCart}>
|
||
加入购物车
|
||
</button>
|
||
</div>
|
||
</article>
|
||
);
|
||
};
|
||
```
|
||
|
||
```tsx
|
||
// ProductCard.stories.tsx
|
||
import type { Meta, StoryObj } from '@storybook/react';
|
||
import { ProductCard } from './ProductCard';
|
||
|
||
const meta: Meta<typeof ProductCard> = {
|
||
title: 'Components/ProductCard',
|
||
component: ProductCard,
|
||
tags: ['autodocs'],
|
||
parameters: {
|
||
layout: 'centered',
|
||
backgrounds: {
|
||
default: 'light',
|
||
},
|
||
},
|
||
};
|
||
|
||
export default meta;
|
||
type Story = StoryObj<typeof ProductCard>;
|
||
|
||
export const Default: Story = {
|
||
args: {
|
||
image: 'https://images.unsplash.com/photo-1494947665470-20322015e3a8',
|
||
title: '袋鼠岛一日游',
|
||
location: '阿德莱德出发',
|
||
rating: 4.8,
|
||
reviewCount: 126,
|
||
price: 389,
|
||
tags: ['热卖', '含午餐'],
|
||
},
|
||
};
|
||
|
||
export const WithDiscount: Story = {
|
||
args: {
|
||
...Default.args,
|
||
originalPrice: 499,
|
||
tags: ['特惠', '限时'],
|
||
},
|
||
};
|
||
|
||
export const Grid: Story = {
|
||
render: () => (
|
||
<div style={{
|
||
display: 'grid',
|
||
gridTemplateColumns: 'repeat(3, 300px)',
|
||
gap: '1.5rem'
|
||
}}>
|
||
<ProductCard
|
||
image="https://images.unsplash.com/photo-1494947665470-20322015e3a8"
|
||
title="袋鼠岛一日游"
|
||
location="阿德莱德出发"
|
||
rating={4.8}
|
||
reviewCount={126}
|
||
price={389}
|
||
tags={['热卖']}
|
||
/>
|
||
<ProductCard
|
||
image="https://images.unsplash.com/photo-1506905925346-21bda4d32df4"
|
||
title="巴罗莎谷酒庄之旅"
|
||
location="阿德莱德出发"
|
||
rating={4.9}
|
||
reviewCount={89}
|
||
price={299}
|
||
originalPrice={399}
|
||
tags={['特惠', '含品酒']}
|
||
/>
|
||
<ProductCard
|
||
image="https://images.unsplash.com/photo-1540202403-b7abd6747a18"
|
||
title="海豚巡航体验"
|
||
location="格雷尔海滩"
|
||
rating={4.7}
|
||
reviewCount={234}
|
||
price={159}
|
||
tags={['亲子']}
|
||
/>
|
||
</div>
|
||
),
|
||
};
|
||
```
|
||
|
||
### 2. 按钮组件 (Button)
|
||
|
||
```tsx
|
||
// Button.tsx
|
||
import React from 'react';
|
||
import styles from './Button.module.css';
|
||
|
||
export interface ButtonProps {
|
||
variant?: 'primary' | 'secondary' | 'outline' | 'ghost' | 'danger';
|
||
size?: 'sm' | 'md' | 'lg';
|
||
fullWidth?: boolean;
|
||
loading?: boolean;
|
||
disabled?: boolean;
|
||
leftIcon?: React.ReactNode;
|
||
rightIcon?: React.ReactNode;
|
||
children: React.ReactNode;
|
||
onClick?: () => void;
|
||
}
|
||
|
||
export const Button: React.FC<ButtonProps> = ({
|
||
variant = 'primary',
|
||
size = 'md',
|
||
fullWidth = false,
|
||
loading = false,
|
||
disabled = false,
|
||
leftIcon,
|
||
rightIcon,
|
||
children,
|
||
onClick,
|
||
}) => {
|
||
return (
|
||
<button
|
||
className={`
|
||
${styles.button}
|
||
${styles[variant]}
|
||
${styles[size]}
|
||
${fullWidth ? styles.fullWidth : ''}
|
||
`}
|
||
disabled={disabled || loading}
|
||
onClick={onClick}
|
||
>
|
||
{loading ? (
|
||
<span className={styles.spinner} />
|
||
) : (
|
||
<>
|
||
{leftIcon && <span className={styles.icon}>{leftIcon}</span>}
|
||
<span>{children}</span>
|
||
{rightIcon && <span className={styles.icon}>{rightIcon}</span>}
|
||
</>
|
||
)}
|
||
</button>
|
||
);
|
||
};
|
||
```
|
||
|
||
---
|
||
|
||
## Storybook 配置
|
||
|
||
### .storybook/main.ts
|
||
|
||
```ts
|
||
import type { StorybookConfig } from '@storybook/react-vite';
|
||
|
||
const config: StorybookConfig = {
|
||
stories: ['../src/**/*.stories.@(js|jsx|ts|tsx)'],
|
||
addons: [
|
||
'@storybook/addon-links',
|
||
'@storybook/addon-essentials',
|
||
'@storybook/addon-interactions',
|
||
'@storybook/addon-a11y',
|
||
],
|
||
framework: {
|
||
name: '@storybook/react-vite',
|
||
options: {},
|
||
},
|
||
docs: {
|
||
autodocs: 'tag',
|
||
},
|
||
};
|
||
|
||
export default config;
|
||
```
|
||
|
||
### .storybook/preview.ts
|
||
|
||
```ts
|
||
import type { Preview } from '@storybook/react';
|
||
import '../src/styles/variables.css';
|
||
import '../src/styles/typography.css';
|
||
|
||
const preview: Preview = {
|
||
parameters: {
|
||
actions: { argTypesRegex: '^on[A-Z].*' },
|
||
controls: {
|
||
matchers: {
|
||
color: /(background|color)$/i,
|
||
date: /Date$/,
|
||
},
|
||
},
|
||
backgrounds: {
|
||
default: 'light',
|
||
values: [
|
||
{ name: 'light', value: '#ffffff' },
|
||
{ name: 'gray', value: '#f8fafc' },
|
||
{ name: 'dark', value: '#0f172a' },
|
||
],
|
||
},
|
||
},
|
||
globalTypes: {
|
||
theme: {
|
||
description: 'Global theme for components',
|
||
defaultValue: 'light',
|
||
toolbar: {
|
||
title: 'Theme',
|
||
icon: 'circlehollow',
|
||
items: ['light', 'dark'],
|
||
dynamicTitle: true,
|
||
},
|
||
},
|
||
},
|
||
};
|
||
|
||
export default preview;
|
||
```
|
||
|
||
---
|
||
|
||
## 快速启动命令
|
||
|
||
### 创建新组件
|
||
|
||
```bash
|
||
# 创建组件目录
|
||
mkdir -p src/components/ComponentName
|
||
|
||
# 创建文件
|
||
touch src/components/ComponentName/{ComponentName.tsx,ComponentName.stories.tsx,ComponentName.module.css,index.ts}
|
||
```
|
||
|
||
### 安装 Storybook
|
||
|
||
```bash
|
||
# 初始化 Storybook
|
||
npx storybook@latest init
|
||
|
||
# 安装额外插件
|
||
npm install -D @storybook/addon-a11y @storybook/addon-interactions
|
||
|
||
# 启动 Storybook
|
||
npm run storybook
|
||
```
|
||
|
||
---
|
||
|
||
## 设计检查清单
|
||
|
||
### 组件质量检查
|
||
|
||
- [ ] Props 接口定义完整,带 JSDoc 注释
|
||
- [ ] 支持必要的变体(variant)和尺寸(size)
|
||
- [ ] 处理禁用和加载状态
|
||
- [ ] 支持自定义 className
|
||
- [ ] 键盘可访问性
|
||
- [ ] 屏幕阅读器友好
|
||
|
||
### Storybook 质量检查
|
||
|
||
- [ ] 所有变体都有对应 Story
|
||
- [ ] argTypes 配置完整
|
||
- [ ] 包含组件文档描述
|
||
- [ ] 交互状态可测试
|
||
- [ ] 响应式展示
|
||
|
||
### 视觉质量检查
|
||
|
||
- [ ] 字体选择有特色
|
||
- [ ] 配色方案协调
|
||
- [ ] 动画流畅自然
|
||
- [ ] 间距一致
|
||
- [ ] 暗色主题支持
|