Skip to content

自定义页面

Rspress 允许您创建自定义页面布局,超越默认的文档格式。本章探讨如何使用 React 组件构建独特的自定义页面。

什么是自定义页面?

概述

自定义页面允许您创建不遵循标准文档布局的页面。它们非常适合:

  • 着陆页
  • 产品展示
  • 仪表板
  • 交互式演示
  • 营销页面
  • 定制工具

标准页面 vs 自定义页面

标准页面:

markdown
# 文档标题

普通的 Markdown 内容...

自定义页面:

tsx
export default function CustomPage() {
  return (
    <div className="custom-layout">
      <Hero />
      <Features />
      <CTA />
    </div>
  );
}

创建自定义页面

基本自定义页面

创建带有自定义布局的页面:

tsx
// docs/custom-page.tsx
import React from 'react';

export default function CustomPage() {
  return (
    <div className="custom-page">
      <header className="hero">
        <h1>欢迎来到我的项目</h1>
        <p>使用 Rspress 构建出色的文档</p>
        <button className="cta-button">开始使用</button>
      </header>

      <section className="features">
        <div className="feature">
          <h3>快速</h3>
          <p>闪电般快速的构建</p>
        </div>
        <div className="feature">
          <h3>灵活</h3>
          <p>完全可定制</p>
        </div>
        <div className="feature">
          <h3>现代</h3>
          <p>现代开发体验</p>
        </div>
      </section>
    </div>
  );
}

带样式的自定义页面

使用 CSS 模块添加样式:

tsx
// docs/landing.tsx
import React from 'react';
import styles from './landing.module.css';

export default function LandingPage() {
  return (
    <div className={styles.container}>
      <div className={styles.hero}>
        <h1 className={styles.title}>下一代文档</h1>
        <p className={styles.subtitle}>
          快速、现代且强大
        </p>
        <div className={styles.actions}>
          <a href="/guide/quick-start" className={styles.primary}>
            快速开始
          </a>
          <a href="/guide/introduction" className={styles.secondary}>
            了解更多
          </a>
        </div>
      </div>
    </div>
  );
}
css
/* docs/landing.module.css */
.container {
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
}

.hero {
  text-align: center;
  max-width: 900px;
  padding: 0 20px;
}

.title {
  font-size: 4rem;
  font-weight: bold;
  margin-bottom: 1rem;
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
}

.subtitle {
  font-size: 1.5rem;
  color: #666;
  margin-bottom: 2rem;
}

.actions {
  display: flex;
  gap: 1rem;
  justify-content: center;
}

.primary, .secondary {
  padding: 12px 32px;
  border-radius: 8px;
  text-decoration: none;
  font-weight: 600;
  transition: all 0.3s;
}

.primary {
  background: #667eea;
  color: white;
}

.primary:hover {
  background: #5568d3;
  transform: translateY(-2px);
  box-shadow: 0 10px 20px rgba(102, 126, 234, 0.3);
}

.secondary {
  background: white;
  color: #667eea;
  border: 2px solid #667eea;
}

.secondary:hover {
  background: #f0f0f0;
}

页面布局模式

全宽布局

创建不受容器限制的全宽布局:

tsx
// docs/showcase.tsx
import React from 'react';

export default function Showcase() {
  return (
    <div style={{ width: '100vw', margin: 0, padding: 0 }}>
      <section style={{
        background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
        padding: '100px 20px',
        color: 'white',
        textAlign: 'center'
      }}>
        <h1>展示</h1>
        <p>使用我们的技术构建的令人惊叹的项目</p>
      </section>

      <section style={{ padding: '60px 20px' }}>
        <div style={{
          maxWidth: '1200px',
          margin: '0 auto',
          display: 'grid',
          gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))',
          gap: '20px'
        }}>
          <ProjectCard
            title="项目 1"
            description="令人惊叹的文档站点"
            image="/project1.png"
          />
          <ProjectCard
            title="项目 2"
            description="交互式指南"
            image="/project2.png"
          />
          <ProjectCard
            title="项目 3"
            description="API 文档"
            image="/project3.png"
          />
        </div>
      </section>
    </div>
  );
}

function ProjectCard({ title, description, image }) {
  return (
    <div style={{
      border: '1px solid #eee',
      borderRadius: '8px',
      overflow: 'hidden',
      transition: 'transform 0.3s'
    }}>
      <img src={image} alt={title} style={{ width: '100%', height: '200px', objectFit: 'cover' }} />
      <div style={{ padding: '20px' }}>
        <h3>{title}</h3>
        <p>{description}</p>
      </div>
    </div>
  );
}

分栏布局

创建多列布局:

tsx
// docs/comparison.tsx
import React from 'react';

export default function Comparison() {
  return (
    <div style={{ maxWidth: '1200px', margin: '0 auto', padding: '40px 20px' }}>
      <h1 style={{ textAlign: 'center', marginBottom: '60px' }}>
        选择正确的方案
      </h1>

      <div style={{
        display: 'grid',
        gridTemplateColumns: 'repeat(3, 1fr)',
        gap: '30px'
      }}>
        <PricingCard
          title="免费"
          price="$0"
          features={[
            '最多 5 个项目',
            '基本支持',
            '社区访问'
          ]}
        />
        <PricingCard
          title="专业"
          price="$29"
          features={[
            '无限项目',
            '优先支持',
            '高级功能',
            '自定义域名'
          ]}
          highlighted
        />
        <PricingCard
          title="企业"
          price="联系我们"
          features={[
            '一切专业功能',
            '专属支持',
            'SLA 保证',
            '定制开发'
          ]}
        />
      </div>
    </div>
  );
}

function PricingCard({ title, price, features, highlighted = false }) {
  return (
    <div style={{
      border: highlighted ? '3px solid #667eea' : '1px solid #eee',
      borderRadius: '12px',
      padding: '30px',
      textAlign: 'center',
      position: 'relative',
      transform: highlighted ? 'scale(1.05)' : 'scale(1)',
      boxShadow: highlighted ? '0 10px 40px rgba(0,0,0,0.1)' : 'none'
    }}>
      {highlighted && (
        <div style={{
          position: 'absolute',
          top: '-15px',
          left: '50%',
          transform: 'translateX(-50%)',
          background: '#667eea',
          color: 'white',
          padding: '5px 20px',
          borderRadius: '20px',
          fontSize: '12px',
          fontWeight: 'bold'
        }}>
          最受欢迎
        </div>
      )}
      <h3 style={{ fontSize: '24px', marginBottom: '10px' }}>{title}</h3>
      <div style={{ fontSize: '36px', fontWeight: 'bold', marginBottom: '20px' }}>
        {price}
      </div>
      <ul style={{ listStyle: 'none', padding: 0, marginBottom: '30px' }}>
        {features.map((feature, i) => (
          <li key={i} style={{ padding: '10px 0', borderBottom: '1px solid #f0f0f0' }}>
            ✓ {feature}
          </li>
        ))}
      </ul>
      <button style={{
        width: '100%',
        padding: '12px',
        background: highlighted ? '#667eea' : '#f0f0f0',
        color: highlighted ? 'white' : '#333',
        border: 'none',
        borderRadius: '8px',
        fontSize: '16px',
        fontWeight: '600',
        cursor: 'pointer'
      }}>
        开始使用
      </button>
    </div>
  );
}

交互式页面

带状态的交互页面

创建具有交互性的页面:

tsx
// docs/demo.tsx
import React, { useState } from 'react';

export default function DemoPage() {
  const [activeTab, setActiveTab] = useState('overview');
  const [code, setCode] = useState('console.log("Hello, World!");');
  const [output, setOutput] = useState('');

  const runCode = () => {
    try {
      // 警告: eval 在生产中不安全
      const result = eval(code);
      setOutput(String(result));
    } catch (error) {
      setOutput(`错误: ${error.message}`);
    }
  };

  return (
    <div style={{ maxWidth: '1200px', margin: '0 auto', padding: '40px 20px' }}>
      <h1>交互式演示</h1>

      {/* 标签页导航 */}
      <div style={{
        display: 'flex',
        gap: '10px',
        borderBottom: '2px solid #eee',
        marginBottom: '30px'
      }}>
        {['概览', '代码', '输出'].map((tab) => (
          <button
            key={tab}
            onClick={() => setActiveTab(tab.toLowerCase())}
            style={{
              padding: '10px 20px',
              background: 'none',
              border: 'none',
              borderBottom: activeTab === tab.toLowerCase() ? '3px solid #667eea' : 'none',
              color: activeTab === tab.toLowerCase() ? '#667eea' : '#666',
              fontWeight: activeTab === tab.toLowerCase() ? 'bold' : 'normal',
              cursor: 'pointer'
            }}
          >
            {tab}
          </button>
        ))}
      </div>

      {/* 标签页内容 */}
      {activeTab === 'overview' && (
        <div>
          <h2>概览</h2>
          <p>这是一个交互式代码演示场。尝试编写一些 JavaScript 代码并运行它!</p>
        </div>
      )}

      {activeTab === 'code' && (
        <div>
          <h2>代码编辑器</h2>
          <textarea
            value={code}
            onChange={(e) => setCode(e.target.value)}
            style={{
              width: '100%',
              height: '300px',
              fontFamily: 'monospace',
              fontSize: '14px',
              padding: '15px',
              border: '1px solid #ddd',
              borderRadius: '8px'
            }}
          />
          <button
            onClick={runCode}
            style={{
              marginTop: '10px',
              padding: '10px 20px',
              background: '#667eea',
              color: 'white',
              border: 'none',
              borderRadius: '8px',
              cursor: 'pointer'
            }}
          >
            运行代码
          </button>
        </div>
      )}

      {activeTab === 'output' && (
        <div>
          <h2>输出</h2>
          <pre style={{
            background: '#f5f5f5',
            padding: '20px',
            borderRadius: '8px',
            minHeight: '100px'
          }}>
            {output || '尚无输出'}
          </pre>
        </div>
      )}
    </div>
  );
}

表单页面

创建带验证的表单:

tsx
// docs/contact.tsx
import React, { useState } from 'react';

export default function ContactPage() {
  const [formData, setFormData] = useState({
    name: '',
    email: '',
    message: ''
  });
  const [errors, setErrors] = useState({});
  const [submitted, setSubmitted] = useState(false);

  const validate = () => {
    const newErrors = {};
    if (!formData.name) newErrors.name = '姓名为必填项';
    if (!formData.email) newErrors.email = '电子邮件为必填项';
    else if (!/\S+@\S+\.\S+/.test(formData.email)) {
      newErrors.email = '电子邮件无效';
    }
    if (!formData.message) newErrors.message = '消息为必填项';
    return newErrors;
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    const newErrors = validate();
    if (Object.keys(newErrors).length === 0) {
      // 提交表单
      console.log('表单提交:', formData);
      setSubmitted(true);
    } else {
      setErrors(newErrors);
    }
  };

  if (submitted) {
    return (
      <div style={{
        maxWidth: '600px',
        margin: '100px auto',
        textAlign: 'center',
        padding: '40px',
        background: '#f0f8ff',
        borderRadius: '12px'
      }}>
        <h2>感谢您!</h2>
        <p>您的消息已成功发送。我们会尽快回复您。</p>
      </div>
    );
  }

  return (
    <div style={{ maxWidth: '600px', margin: '0 auto', padding: '40px 20px' }}>
      <h1>联系我们</h1>
      <p>有问题吗?我们很乐意听取您的意见。</p>

      <form onSubmit={handleSubmit} style={{ marginTop: '30px' }}>
        <div style={{ marginBottom: '20px' }}>
          <label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold' }}>
            姓名
          </label>
          <input
            type="text"
            value={formData.name}
            onChange={(e) => setFormData({ ...formData, name: e.target.value })}
            style={{
              width: '100%',
              padding: '10px',
              border: errors.name ? '2px solid red' : '1px solid #ddd',
              borderRadius: '6px'
            }}
          />
          {errors.name && <span style={{ color: 'red', fontSize: '14px' }}>{errors.name}</span>}
        </div>

        <div style={{ marginBottom: '20px' }}>
          <label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold' }}>
            电子邮件
          </label>
          <input
            type="email"
            value={formData.email}
            onChange={(e) => setFormData({ ...formData, email: e.target.value })}
            style={{
              width: '100%',
              padding: '10px',
              border: errors.email ? '2px solid red' : '1px solid #ddd',
              borderRadius: '6px'
            }}
          />
          {errors.email && <span style={{ color: 'red', fontSize: '14px' }}>{errors.email}</span>}
        </div>

        <div style={{ marginBottom: '20px' }}>
          <label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold' }}>
            消息
          </label>
          <textarea
            value={formData.message}
            onChange={(e) => setFormData({ ...formData, message: e.target.value })}
            rows={6}
            style={{
              width: '100%',
              padding: '10px',
              border: errors.message ? '2px solid red' : '1px solid #ddd',
              borderRadius: '6px',
              fontFamily: 'inherit'
            }}
          />
          {errors.message && <span style={{ color: 'red', fontSize: '14px' }}>{errors.message}</span>}
        </div>

        <button
          type="submit"
          style={{
            padding: '12px 40px',
            background: '#667eea',
            color: 'white',
            border: 'none',
            borderRadius: '8px',
            fontSize: '16px',
            fontWeight: '600',
            cursor: 'pointer'
          }}
        >
          发送消息
        </button>
      </form>
    </div>
  );
}

使用 Rspress 功能

访问主题上下文

在自定义页面中使用主题功能:

tsx
// docs/themed-page.tsx
import React from 'react';
import { usePageData } from 'rspress/runtime';

export default function ThemedPage() {
  const pageData = usePageData();

  return (
    <div style={{ padding: '40px 20px' }}>
      <h1>页面信息</h1>
      <p>当前页面: {pageData.page.title}</p>
      <p>路由路径: {pageData.page.routePath}</p>

      <h2>站点配置</h2>
      <p>站点标题: {pageData.siteData.title}</p>
      <p>站点描述: {pageData.siteData.description}</p>
    </div>
  );
}

包含导航

在自定义页面中添加导航:

tsx
// docs/custom-with-nav.tsx
import React from 'react';
import { Link } from 'rspress/runtime';

export default function CustomWithNav() {
  return (
    <div>
      {/* 自定义导航 */}
      <nav style={{
        background: '#667eea',
        padding: '20px',
        color: 'white'
      }}>
        <div style={{
          maxWidth: '1200px',
          margin: '0 auto',
          display: 'flex',
          gap: '30px',
          alignItems: 'center'
        }}>
          <Link to="/" style={{ color: 'white', textDecoration: 'none', fontWeight: 'bold' }}>
            首页
          </Link>
          <Link to="/guide" style={{ color: 'white', textDecoration: 'none' }}>
            指南
          </Link>
          <Link to="/api" style={{ color: 'white', textDecoration: 'none' }}>
            API
          </Link>
        </div>
      </nav>

      {/* 页面内容 */}
      <div style={{ maxWidth: '1200px', margin: '40px auto', padding: '0 20px' }}>
        <h1>自定义内容</h1>
        <p>这个页面有自定义导航,但仍然使用 Rspress 路由。</p>
      </div>
    </div>
  );
}

页面模板

创建可重用模板

构建可重用的页面模板:

tsx
// components/PageTemplate.tsx
import React from 'react';

interface PageTemplateProps {
  title: string;
  subtitle?: string;
  children: React.ReactNode;
  fullWidth?: boolean;
}

export function PageTemplate({
  title,
  subtitle,
  children,
  fullWidth = false
}: PageTemplateProps) {
  return (
    <div>
      {/* 页眉 */}
      <header style={{
        background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
        padding: '60px 20px',
        color: 'white',
        textAlign: 'center'
      }}>
        <h1 style={{ fontSize: '3rem', marginBottom: '10px' }}>{title}</h1>
        {subtitle && <p style={{ fontSize: '1.25rem', opacity: 0.9 }}>{subtitle}</p>}
      </header>

      {/* 内容 */}
      <main style={{
        maxWidth: fullWidth ? '100%' : '1200px',
        margin: '0 auto',
        padding: '40px 20px'
      }}>
        {children}
      </main>

      {/* 页脚 */}
      <footer style={{
        background: '#f5f5f5',
        padding: '40px 20px',
        textAlign: 'center',
        marginTop: '60px'
      }}>
        <p>&copy; 2024 我的项目。保留所有权利。</p>
      </footer>
    </div>
  );
}

使用模板:

tsx
// docs/features.tsx
import React from 'react';
import { PageTemplate } from '../components/PageTemplate';

export default function FeaturesPage() {
  return (
    <PageTemplate
      title="功能"
      subtitle="探索使我们的产品强大的功能"
    >
      <div style={{
        display: 'grid',
        gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))',
        gap: '30px',
        marginTop: '40px'
      }}>
        <FeatureCard
          icon="⚡"
          title="闪电般快速"
          description="使用 Vite 构建,实现极快的开发和构建时间"
        />
        <FeatureCard
          icon="🎨"
          title="可定制"
          description="完全的主题控制和样式定制"
        />
        <FeatureCard
          icon="📱"
          title="响应式"
          description="在所有设备上都完美运行"
        />
      </div>
    </PageTemplate>
  );
}

function FeatureCard({ icon, title, description }) {
  return (
    <div style={{
      padding: '30px',
      border: '1px solid #eee',
      borderRadius: '12px',
      textAlign: 'center',
      transition: 'transform 0.3s, box-shadow 0.3s'
    }}>
      <div style={{ fontSize: '3rem', marginBottom: '20px' }}>{icon}</div>
      <h3 style={{ marginBottom: '10px' }}>{title}</h3>
      <p style={{ color: '#666' }}>{description}</p>
    </div>
  );
}

最佳实践

性能优化

  1. 代码分割
tsx
import { lazy, Suspense } from 'react';

const HeavyComponent = lazy(() => import('./HeavyComponent'));

export default function Page() {
  return (
    <Suspense fallback={<div>加载中...</div>}>
      <HeavyComponent />
    </Suspense>
  );
}
  1. 图片优化
tsx
import { Image } from 'rspress/runtime';

export default function Page() {
  return (
    <Image
      src="/large-image.jpg"
      alt="描述"
      loading="lazy"
      width={800}
      height={600}
    />
  );
}

响应式设计

使用媒体查询创建响应式页面:

tsx
export default function ResponsivePage() {
  return (
    <div style={{
      display: 'grid',
      gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))',
      gap: '20px',
      padding: '20px'
    }}>
      {/* 内容自动调整到屏幕大小 */}
    </div>
  );
}

SEO 优化

为自定义页面添加元数据:

tsx
// docs/landing.tsx
import { Head } from 'rspress/runtime';

export default function LandingPage() {
  return (
    <>
      <Head>
        <title>我的着陆页 - 项目名称</title>
        <meta name="description" content="这是一个令人惊叹的着陆页" />
        <meta property="og:title" content="我的着陆页" />
        <meta property="og:description" content="这是一个令人惊叹的着陆页" />
      </Head>

      <div>{/* 页面内容 */}</div>
    </>
  );
}

故障排除

常见问题

  1. 样式冲突
tsx
// 使用 CSS 模块避免冲突
import styles from './page.module.css';

export default function Page() {
  return <div className={styles.container}>内容</div>;
}
  1. 路由问题
tsx
// 使用 Rspress Link 组件进行正确的路由
import { Link } from 'rspress/runtime';

<Link to="/guide">转到指南</Link>
  1. 状态管理
tsx
// 对于复杂状态,使用 React Context
import { createContext, useContext } from 'react';

const AppContext = createContext();

export function useAppContext() {
  return useContext(AppContext);
}

下一步


最佳实践

  • 保持自定义页面性能良好
  • 为自定义页面使用 TypeScript
  • 实现适当的错误处理
  • 测试所有设备上的响应性
  • 为 SEO 添加适当的元数据

注意事项

  • 避免在自定义页面中使用 eval()
  • 清理所有用户输入
  • 谨慎处理敏感数据
  • 测试跨浏览器兼容性