金句闪卡生成器

功能介绍:

  1. 四种预设风格

    • 素雅白:极简主义,适合打印或清爽的排版。

    • 水墨黑:深色模式,非常有质感,文字如月光般浮现。

    • 泛黄纸:使用了书法字体(马善政毛笔体),配合纸张纹理,非常适合古风句子(如第一句和第三句)。

    • 现代灰:适合发朋友圈的现代感设计。

  2. 智能排版

    • 针对"落花有意..."和"人情如纸..."这种对仗工整的诗句,程序会自动采用竖排版(古籍风格)。

    • 针对长难句(如最后一句),会自动调整为两端对齐,保证阅读舒适度。

  3. 保存:点击按钮即可生成高分辨率图片。

html 复制代码
import React, { useState, useEffect, useRef } from 'react';
import { Download, Copy, Quote, Palette, Type, LayoutTemplate } from 'lucide-react';

const QuoteCardGenerator = () => {
  const [selectedQuoteIndex, setSelectedQuoteIndex] = useState(0);
  const [currentTheme, setCurrentTheme] = useState('zen');
  const cardRef = useRef(null);
  const [fontLoaded, setFontLoaded] = useState(false);

  const quotes = [
    {
      id: 1,
      text: "落花有意随流水\n流水无情恋落花",
      author: "冯梦龙",
      tag: "情感",
      layout: "vertical" // 竖排更适合这首诗
    },
    {
      id: 2,
      text: "要在路上找朋友\n而不是带朋友上路",
      author: "佚名",
      tag: "社交",
      layout: "center"
    },
    {
      id: 3,
      text: "人情如纸张张薄\n世事如棋局局新",
      author: "增广贤文",
      tag: "世态",
      layout: "vertical"
    },
    {
      id: 4,
      text: "一个人的精神寄托,可以是音乐,可以是书籍,可以是运动,可以是工作,可以是山川湖海,唯独不可以是人。",
      author: "佚名",
      tag: "自我",
      layout: "justify"
    }
  ];

  const themes = {
    zen: {
      name: "素雅白",
      bg: "bg-stone-50",
      text: "text-stone-800",
      accent: "text-red-700",
      border: "border-stone-800",
      font: "font-serif",
      texture: "opacity-10"
    },
    ink: {
      name: "水墨黑",
      bg: "bg-stone-900",
      text: "text-stone-200",
      accent: "text-stone-400",
      border: "border-stone-600",
      font: "font-serif",
      texture: "opacity-20"
    },
    paper: {
      name: "泛黄纸",
      bg: "bg-[#fdfbf7]",
      text: "text-[#4a4036]",
      accent: "text-[#8c7b6c]",
      border: "border-[#4a4036]",
      font: "font-serif", //实际上会加载书法字体
      texture: "opacity-50" // 模拟纸张
    },
    modern: {
      name: "现代灰",
      bg: "bg-gray-100",
      text: "text-gray-900",
      accent: "text-blue-600",
      border: "border-gray-300",
      font: "font-sans",
      texture: "opacity-0"
    }
  };

  useEffect(() => {
    // Load Fonts and html2canvas
    const link = document.createElement('link');
    link.href = 'https://fonts.googleapis.com/css2?family=Noto+Serif+SC:wght@300;400;700&family=Ma+Shan+Zheng&display=swap';
    link.rel = 'stylesheet';
    document.head.appendChild(link);

    const script = document.createElement('script');
    script.src = "https://html2canvas.hertzen.com/dist/html2canvas.min.js";
    script.onload = () => setFontLoaded(true);
    document.body.appendChild(script);

    return () => {
      document.head.removeChild(link);
      document.body.removeChild(script);
    };
  }, []);

  const handleDownload = async () => {
    if (!window.html2canvas || !cardRef.current) return;
    
    try {
      const canvas = await window.html2canvas(cardRef.current, {
        scale: 3, // Higher resolution
        backgroundColor: null,
        logging: false,
        useCORS: true
      });
      
      const link = document.createElement('a');
      link.download = `quote-card-${Date.now()}.png`;
      link.href = canvas.toDataURL('image/png');
      link.click();
    } catch (err) {
      console.error("Error generating image:", err);
      alert("生成图片失败,请尝试截图保存。");
    }
  };

  const currentQ = quotes[selectedQuoteIndex];
  const theme = themes[currentTheme];

  // Helper to determine font family based on theme
  const getFontFamily = () => {
    if (currentTheme === 'paper' || currentTheme === 'ink') return "'Ma Shan Zheng', cursive";
    if (currentTheme === 'zen') return "'Noto Serif SC', serif";
    return "'Noto Serif SC', serif"; 
  };

  return (
    <div className="min-h-screen bg-gray-50 text-gray-800 p-4 flex flex-col items-center font-sans">
      <header className="mb-6 text-center">
        <h1 className="text-2xl font-bold text-gray-800 flex items-center justify-center gap-2">
          <Quote size={20} /> 金句卡片工坊
        </h1>
        <p className="text-sm text-gray-500 mt-1">选择金句,生成你的专属卡片</p>
      </header>

      <div className="flex flex-col lg:flex-row gap-8 w-full max-w-5xl items-start justify-center">
        
        {/* Controls Section */}
        <div className="w-full lg:w-1/3 space-y-6 bg-white p-6 rounded-xl shadow-sm border border-gray-100">
          
          {/* Quote Selection */}
          <div>
            <h3 className="text-sm font-semibold text-gray-400 uppercase tracking-wider mb-3 flex items-center gap-2">
              <Type size={16} /> 选择句子
            </h3>
            <div className="space-y-2 max-h-60 overflow-y-auto pr-1">
              {quotes.map((q, idx) => (
                <button
                  key={q.id}
                  onClick={() => setSelectedQuoteIndex(idx)}
                  className={`w-full text-left p-3 rounded-lg text-sm transition-all border ${
                    selectedQuoteIndex === idx 
                      ? 'bg-blue-50 border-blue-200 text-blue-800 shadow-sm' 
                      : 'hover:bg-gray-50 border-transparent text-gray-600'
                  }`}
                >
                  <p className="line-clamp-2">{q.text}</p>
                </button>
              ))}
            </div>
          </div>

          {/* Theme Selection */}
          <div>
            <h3 className="text-sm font-semibold text-gray-400 uppercase tracking-wider mb-3 flex items-center gap-2">
              <Palette size={16} /> 选择风格
            </h3>
            <div className="grid grid-cols-2 gap-2">
              {Object.keys(themes).map((key) => (
                <button
                  key={key}
                  onClick={() => setCurrentTheme(key)}
                  className={`p-3 rounded-lg text-sm border transition-all flex items-center gap-2 ${
                    currentTheme === key
                      ? 'ring-2 ring-blue-500 ring-offset-1 border-transparent'
                      : 'border-gray-200 hover:border-gray-300'
                  }`}
                >
                  <div className={`w-4 h-4 rounded-full border ${themes[key].bg === 'bg-stone-50' ? 'bg-white border-gray-300' : themes[key].bg}`}></div>
                  {themes[key].name}
                </button>
              ))}
            </div>
          </div>

          {/* Actions */}
          <div className="pt-4 border-t border-gray-100">
             <button
              onClick={handleDownload}
              className="w-full py-3 bg-gray-900 hover:bg-gray-800 text-white rounded-lg flex items-center justify-center gap-2 font-medium transition-colors shadow-lg shadow-gray-200/50"
            >
              <Download size={18} /> 保存卡片
            </button>
            <p className="text-xs text-center text-gray-400 mt-2">
              *如果下载失败,请直接截图
            </p>
          </div>
        </div>

        {/* Preview Section */}
        <div className="flex-1 flex justify-center items-center min-h-[500px] w-full bg-gray-200/50 rounded-xl p-8 border border-gray-200 border-dashed">
          
          {/* The Card Itself */}
          <div 
            ref={cardRef}
            className={`relative overflow-hidden shadow-2xl transition-all duration-500 flex flex-col ${theme.bg} ${theme.text}`}
            style={{
              width: '375px',
              height: '667px', // iPhone 8 dimensions (classic 16:9 ish)
              fontFamily: getFontFamily()
            }}
          >
            {/* Background Texture/Noise */}
            <div className={`absolute inset-0 pointer-events-none ${theme.texture}`} 
                 style={{backgroundImage: `url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noiseFilter'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.65' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noiseFilter)' opacity='0.5'/%3E%3C/svg%3E")`}}>
            </div>

            {/* Decorative Border for Ink/Zen themes */}
            {(currentTheme === 'zen' || currentTheme === 'ink') && (
               <div className={`absolute inset-4 border ${theme.border} opacity-30 pointer-events-none`}></div>
            )}

            {/* Content Container */}
            <div className="relative z-10 flex-1 flex flex-col p-8 h-full">
              
              {/* Header/Date */}
              <div className="flex justify-between items-start opacity-60 text-xs tracking-[0.2em] uppercase mb-8">
                <span>Daily Quote</span>
                <span>{new Date().toLocaleDateString('zh-CN').replace(/\//g, '.')}</span>
              </div>

              {/* Main Text Area */}
              <div className="flex-1 flex items-center justify-center">
                 <div className={`
                    w-full relative
                    ${currentQ.layout === 'vertical' ? 'writing-vertical-rl text-center h-[70%] flex flex-col flex-wrap gap-6 items-center justify-center leading-loose tracking-widest' : ''}
                    ${currentQ.layout === 'center' ? 'text-center' : ''}
                    ${currentQ.layout === 'justify' ? 'text-justify leading-loose' : ''}
                 `}>
                    {/* Decorative quote mark for modern layouts */}
                    {currentQ.layout !== 'vertical' && (
                      <Quote size={32} className={`mb-6 opacity-20 ${theme.accent} ${currentQ.layout === 'center' ? 'mx-auto' : ''}`} />
                    )}

                    <p className={`
                      ${currentQ.layout === 'vertical' ? 'text-2xl' : 'text-xl'} 
                      leading-loose whitespace-pre-line font-medium
                    `}>
                      {currentQ.text}
                    </p>
                 </div>
              </div>

              {/* Footer/Author */}
              <div className="mt-8 pt-6 border-t border-current border-opacity-20 flex justify-between items-end">
                <div>
                   <div className={`w-8 h-1 ${theme.bg === 'bg-stone-900' ? 'bg-white' : 'bg-red-700'} mb-2 opacity-80`}></div>
                   <p className="text-sm font-bold tracking-widest opacity-90">{currentQ.author || "无名"}</p>
                   <p className="text-xs opacity-50 mt-1">#{currentQ.tag}</p>
                </div>
                
                {/* Stamp/Watermark */}
                <div className={`
                  w-16 h-16 border-2 rounded-full flex items-center justify-center opacity-80
                  ${theme.bg === 'bg-stone-900' ? 'border-stone-700 text-stone-700' : 'border-red-800/30 text-red-800/30'}
                `}>
                  <span className="text-xs transform -rotate-12 font-serif">阅己</span>
                </div>
              </div>

            </div>
          </div>
        </div>
      </div>
    </div>
  );
};

export default QuoteCardGenerator;
相关推荐
yugi9878381 小时前
TDOA算法MATLAB实现:到达时间差定位
前端·算法·matlab
s***35301 小时前
SpringMVC新版本踩坑[已解决]
android·前端·后端
涔溪1 小时前
深入了解 Vite 的核心特性 —— 开发服务器(Dev Server)和热更新(HMR)的底层工作机制
前端·vite
前端一课1 小时前
【vue高频面试题】第 14 题:Vue3 中虚拟 DOM 是什么?为什么要用?如何提升性能?
前端·面试
前端一课1 小时前
【vue高频面试题】第 17 题:Vue3 虚拟 DOM 与 PatchFlag 原理 + 静态节点提升
前端·面试
前端一课1 小时前
【vue高频面试题】第 13 题:Vue 的 `nextTick` 原理是什么?为什么需要它?
前端·面试
前端一课1 小时前
【vue高频面试题】第 12 题:Vue(尤其 Vue3)中父子组件通信方式有哪些?区别是什么?
前端·面试
前端一课1 小时前
解释watch和computed的原理
前端·面试
前端一课1 小时前
【vue高频面试题】第 18 题:Vue3 响应式原理中的 effect、依赖收集与依赖触发
前端·面试