QuoteCard.jsx 9.89 KB
Newer Older
1 2 3
import React, { useState, useRef } from 'react';
import styled from 'styled-components';
import html2canvas from 'html2canvas';
4
import { useTranslation } from '../js/i18n';
fisherdaddy's avatar
fisherdaddy committed
5
import { useScrollToTop } from '../hooks/useScrollToTop';
6 7 8

// 更新中文字体数组,包含显示名称和 CSS 字体族名称
const chineseFonts = [
9 10 11 12
    { name: 'system', value: '-apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif' },
    { name: 'sans', value: '"PingFang SC", "Microsoft YaHei", "Helvetica Neue", Helvetica, Arial, sans-serif' },
    { name: 'serif', value: 'Georgia, "Nimbus Roman No9 L", "Songti SC", "Noto Serif CJK SC", "Source Han Serif SC", "Source Han Serif CN", STSong, "AR PL New Sung", "AR PL SungtiL GB", NSimSun, SimSun, serif' },
    { name: 'mono', value: '"SF Mono", SFMono-Regular, Consolas, "Liberation Mono", Menlo, Courier, "Noto Sans Mono CJK SC", monospace' },
13 14 15 16 17 18 19 20 21 22 23 24 25
  ];

// 定义可选的英文字体
const englishFonts = [
  { name: 'Arial', value: 'Arial' },
  { name: 'Times New Roman', value: 'Times New Roman' },
  { name: 'Georgia', value: 'Georgia' },
  { name: 'Verdana', value: 'Verdana' },
  { name: 'Courier New', value: 'Courier New' },
];

// 定义可选的背景颜色
const backgroundOptions = [
26 27 28
  { name: 'white', value: '#FFFFFF' },
  { name: 'dark', value: '#333333' }, // 修改为深灰色
  { name: 'paper', value: '#fdf6e3' },
29 30 31 32
];

const Container = styled.div`
  min-height: 100vh;
33
  background: linear-gradient(135deg, #f5f7ff 0%, #ffffff 100%);
34
  padding: 4rem 2rem 2rem;
35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
  position: relative;
  
  &::before {
    content: '';
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background: 
      linear-gradient(90deg, rgba(99, 102, 241, 0.05) 1px, transparent 1px),
      linear-gradient(rgba(99, 102, 241, 0.05) 1px, transparent 1px);
    background-size: 20px 20px;
    pointer-events: none;
  }
50 51
`;

52 53 54 55 56 57 58
const ContentWrapper = styled.div`
  display: flex;
  gap: 2rem;
  max-width: 1400px;
  margin: 0 auto;
  position: relative;
  z-index: 1;
59

60 61 62 63
  @media (max-width: 768px) {
    flex-direction: column;
  }
`;
64 65 66

const InputContainer = styled.div`
  flex: 1;
67 68 69 70 71 72
  background: rgba(255, 255, 255, 0.8);
  backdrop-filter: blur(10px);
  border-radius: 16px;
  padding: 1.5rem;
  box-shadow: 0 8px 32px rgba(99, 102, 241, 0.1);
  border: 1px solid rgba(255, 255, 255, 0.2);
73 74
  display: flex;
  flex-direction: column;
75 76 77 78 79 80 81 82 83 84 85
  gap: 1rem;
`;

const TitleLabel = styled.h2`
  font-size: 1.8rem;
  margin-bottom: 1.5rem;
  background: linear-gradient(135deg, #6366F1 0%, #4F46E5 100%);
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  font-weight: 700;
  letter-spacing: -0.02em;
86 87 88 89
`;

const PreviewContainer = styled.div`
  flex: 1;
90 91 92 93 94
  background: ${(props) => props.$bgColor || '#333333'};
  padding: 2rem;
  border-radius: 16px;
  box-shadow: 0 8px 32px rgba(99, 102, 241, 0.1);
  border: 1px solid rgba(255, 255, 255, 0.2);
95 96 97 98
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
99 100 101 102 103 104 105 106 107 108 109 110 111 112 113
  min-height: 200px;
  height: fit-content;
  align-self: flex-start;
  transition: all 0.3s ease;
  margin-top: 1rem;

  &:hover {
    transform: translateY(-5px);
    box-shadow: 0 12px 24px rgba(99, 102, 241, 0.15);
  }

  @media (max-width: 768px) {
    margin-top: 2rem;
    width: 100%;
  }
114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147
`;

const QuoteText = styled.div`
  font-size: clamp(16px, 2.5vw, 24px);
  margin-bottom: 16px;
  color: ${(props) => props.$color || '#b8b83b'}; // 使用短暂属性
  text-align: center;
  line-height: 1.5;
  font-family: ${(props) => props.$fontFamily}, serif; // 使用短暂属性
  white-space: pre-wrap;
  word-wrap: break-word;
`;

const QuoteAuthor = styled.div`
  font-size: clamp(14px, 2vw, 18px);
  color: ${(props) => props.$color || '#888'}; // 使用短暂属性
  text-align: center;
  font-family: ${(props) => props.$fontFamily}, sans-serif; // 使用短暂属性
  margin-top: 20px;
`;

const InputText = styled.textarea`
  width: 100%;
  height: 80px;
  padding: 10px;
  border-radius: 8px;
  border: 1px solid #dadce0;
  font-size: 16px;
  font-family: Arial, sans-serif;
  resize: vertical;
`;

const InputField = styled.input`
  width: 100%;
148
  padding: 0.8rem;
149
  border-radius: 8px;
150 151 152 153 154 155 156 157
  border: 1px solid rgba(99, 102, 241, 0.2);
  font-size: 1rem;
  transition: all 0.3s ease;

  &:hover, &:focus {
    border-color: rgba(99, 102, 241, 0.4);
    box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.1);
  }
158 159 160 161
`;

const Select = styled.select`
  width: 100%;
162
  padding: 0.8rem;
163
  border-radius: 8px;
164 165 166 167 168 169 170 171 172
  border: 1px solid rgba(99, 102, 241, 0.2);
  font-size: 1rem;
  transition: all 0.3s ease;
  background: white;
  cursor: pointer;

  &:hover {
    border-color: rgba(99, 102, 241, 0.4);
  }
173 174 175 176 177 178 179 180 181 182
`;

const ColorInput = styled.input`
  width: 100%;
  padding: 5px;
  border: none;
  background-color: transparent;
`;

const Label = styled.label`
183 184 185 186
  font-weight: 600;
  color: #1a1a1a;
  margin-bottom: 0.5rem;
  display: block;
187 188 189
`;

const DownloadButton = styled.button`
190
  background: linear-gradient(135deg, #6366F1 0%, #4F46E5 100%);
191 192
  color: white;
  border: none;
193
  padding: 1rem;
194
  border-radius: 8px;
195
  font-weight: 600;
196
  cursor: pointer;
197 198
  transition: all 0.3s ease;
  margin-top: 1rem;
199 200

  &:hover {
201 202
    transform: translateY(-2px);
    box-shadow: 0 4px 12px rgba(99, 102, 241, 0.2);
203 204 205 206
  }
`;

function QuoteCard() {
fisherdaddy's avatar
fisherdaddy committed
207
  useScrollToTop();
208 209
  const { t } = useTranslation();
  
210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247
  const [chineseText, setChineseText] = useState('');
  const [englishText, setEnglishText] = useState('');
  const [author, setAuthor] = useState('');

  const [chineseFont, setChineseFont] = useState('SimSun');
  const [englishFont, setEnglishFont] = useState('Arial');
  const [fontColor, setFontColor] = useState('#b8b83b');
  const [authorColor, setAuthorColor] = useState('#b8b83b');
  const [bgColor, setBgColor] = useState('#333333');
  const [customBgColor, setCustomBgColor] = useState('#FFFFFF');

  const previewRef = useRef(null);

  const handleBackgroundChange = (e) => {
    const value = e.target.value;
    if (value !== 'custom') {
      setBgColor(value);
    }
  };

  const handleDownload = () => {
    if (previewRef.current) {
      html2canvas(previewRef.current, {
        backgroundColor: null,
        useCORS: true,
      }).then((canvas) => {
        const link = document.createElement('a');
        link.download = 'quote_card.png';
        link.href = canvas.toDataURL('image/png');
        link.click();
      }).catch((err) => {
        console.error('图片下载失败:', err);
      });
    }
  };

  return (
    <Container>
248 249
      <ContentWrapper>
        <InputContainer>
250
          <TitleLabel>{t('tools.quoteCard.title')}</TitleLabel>
251
          <InputText
252
            placeholder={t('tools.quoteCard.chinesePlaceholder')}
253 254 255 256
            value={chineseText}
            onChange={(e) => setChineseText(e.target.value)}
          />
          <InputText
257
            placeholder={t('tools.quoteCard.englishPlaceholder')}
258 259 260 261
            value={englishText}
            onChange={(e) => setEnglishText(e.target.value)}
          />
          <InputField
262
            placeholder={t('tools.quoteCard.authorPlaceholder')}
263 264 265 266
            value={author}
            onChange={(e) => setAuthor(e.target.value)}
          />

267
          <Label>{t('tools.quoteCard.selectChineseFont')}:</Label>
268 269 270
          <Select value={chineseFont} onChange={(e) => setChineseFont(e.target.value)}>
            {chineseFonts.map((font) => (
              <option key={font.value} value={font.value}>
271
                {t(`tools.quoteCard.fonts.${font.name}`)}
272 273 274 275
              </option>
            ))}
          </Select>

276
          <Label>{t('tools.quoteCard.selectEnglishFont')}:</Label>
277 278 279 280 281 282 283 284
          <Select value={englishFont} onChange={(e) => setEnglishFont(e.target.value)}>
            {englishFonts.map((font) => (
              <option key={font.value} value={font.value}>
                {font.name}
              </option>
            ))}
          </Select>

285
          <Label>{t('tools.quoteCard.selectFontColor')}:</Label>
286 287 288 289 290 291
          <ColorInput
            type="color"
            value={fontColor}
            onChange={(e) => setFontColor(e.target.value)}
          />

292
          <Label>{t('tools.quoteCard.selectAuthorColor')}:</Label>
293 294 295 296 297 298
          <ColorInput
            type="color"
            value={authorColor}
            onChange={(e) => setAuthorColor(e.target.value)}
          />

299
          <Label>{t('tools.quoteCard.selectBackground')}:</Label>
300
          <Select
301
            value={backgroundOptions.some((option) => option.value === bgColor) ? bgColor : 'custom'}
302 303 304
            onChange={handleBackgroundChange}
          >
            {backgroundOptions.map((option) => (
305 306
              <option key={option.value} value={option.value}>
                {t(`tools.quoteCard.backgrounds.${option.name}`)}
307 308 309 310 311
              </option>
            ))}
          </Select>
          {bgColor === 'custom' && (
            <>
312
              <Label>{t('tools.quoteCard.customBackground')}:</Label>
313 314 315 316
              <ColorInput
                type="color"
                value={customBgColor}
                onChange={(e) => setBgColor(e.target.value)}
317
                title={t('tools.quoteCard.customBackground')}
318 319 320 321 322
              />
            </>
          )}

          <DownloadButton onClick={handleDownload}>
323
            {t('tools.quoteCard.downloadButton')}
324 325 326
          </DownloadButton>
        </InputContainer>

327
        <PreviewContainer $bgColor={bgColor} $fontColor={fontColor} ref={previewRef}>
328
          <QuoteText $color={fontColor} $fontFamily={chineseFont}>
329
            {chineseText || t('tools.quoteCard.defaultQuote')}
330 331
          </QuoteText>
          <QuoteText $color={fontColor} $fontFamily={englishFont}>
332
            {englishText || t('tools.quoteCard.defaultTranslation')}
333 334
          </QuoteText>
          <QuoteAuthor $color={authorColor} $fontFamily={englishFont}>
335
            —— {author || t('tools.quoteCard.defaultAuthor')}
336 337 338
          </QuoteAuthor>
        </PreviewContainer>
      </ContentWrapper>
339 340 341 342 343
    </Container>
  );
}

export default QuoteCard;