UrlEnDecode.jsx 6.18 KB
Newer Older
fisherdaddy's avatar
fisherdaddy committed
1 2 3
import React, { useState, useCallback } from 'react';
import { useTranslation } from '../js/i18n';
import SEO from './SEO';
4
import styled from 'styled-components';
fisherdaddy's avatar
fisherdaddy committed
5 6
import { usePageLoading } from '../hooks/usePageLoading';
import LoadingOverlay from './LoadingOverlay';
fisherdaddy's avatar
fisherdaddy committed
7

8 9 10 11
// 复用相同的样式组件
const Container = styled.div`
  min-height: 100vh;
  background: linear-gradient(135deg, #f5f7ff 0%, #ffffff 100%);
fisherdaddy's avatar
fisherdaddy committed
12
  padding: 6rem 2rem 2rem;
fisherdaddy's avatar
fisherdaddy committed
13
  position: relative;
14 15 16 17 18 19 20 21 22 23 24 25 26 27
  
  &::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;
  }
fisherdaddy's avatar
fisherdaddy committed
28 29
`;

30 31 32
const ContentWrapper = styled.div`
  max-width: 1400px;
  margin: 0 auto;
fisherdaddy's avatar
fisherdaddy committed
33
  position: relative;
34
  z-index: 1;
fisherdaddy's avatar
fisherdaddy committed
35 36
`;

37 38 39 40 41 42 43 44 45
const Title = 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;
  text-align: center;
fisherdaddy's avatar
fisherdaddy committed
46 47 48
`;

function UrlEncoderDecoder() {
fisherdaddy's avatar
fisherdaddy committed
49
  const { t } = useTranslation();
fisherdaddy's avatar
fisherdaddy committed
50
  const isLoading = usePageLoading();
fisherdaddy's avatar
fisherdaddy committed
51
  const [input, setInput] = useState('');
fisherdaddy's avatar
fisherdaddy committed
52
  const [resultText, setResultText] = useState('');
fisherdaddy's avatar
fisherdaddy committed
53
  const [isCopied, setIsCopied] = useState(false);
54
  const [mode, setMode] = useState('decode');
fisherdaddy's avatar
fisherdaddy committed
55 56 57 58 59 60

  const handleModeChange = (e) => {
    setMode(e.target.value);
    setInput('');
    setResultText('');
  };
fisherdaddy's avatar
fisherdaddy committed
61 62 63 64 65

  const handleInputChange = (e) => {
    const inputValue = e.target.value;
    setInput(inputValue);
    try {
fisherdaddy's avatar
fisherdaddy committed
66 67 68 69 70 71 72
      let result;
      if (mode === 'decode') {
        result = decodeURIComponent(inputValue);
      } else {
        result = encodeURIComponent(inputValue);
      }
      setResultText(result);
fisherdaddy's avatar
fisherdaddy committed
73
    } catch (error) {
fisherdaddy's avatar
fisherdaddy committed
74
      setResultText('Invalid input');
fisherdaddy's avatar
fisherdaddy committed
75 76 77 78
    }
  };

  const handleCopy = useCallback(() => {
fisherdaddy's avatar
fisherdaddy committed
79
    navigator.clipboard.writeText(resultText).then(() => {
fisherdaddy's avatar
fisherdaddy committed
80 81 82
      setIsCopied(true);
      setTimeout(() => setIsCopied(false), 2000);
    });
fisherdaddy's avatar
fisherdaddy committed
83
  }, [resultText]);
fisherdaddy's avatar
fisherdaddy committed
84 85 86

  return (
    <>
fisherdaddy's avatar
fisherdaddy committed
87
      {isLoading && <LoadingOverlay />}
fisherdaddy's avatar
fisherdaddy committed
88
      <SEO
fisherdaddy's avatar
fisherdaddy committed
89 90
        title={t('tools.urlEncodeDecode.title')}
        description={t('tools.urlEncodeDecode.description')}
fisherdaddy's avatar
fisherdaddy committed
91
      />
92 93 94
      <Container>
        <ContentWrapper>
          <Title>{t('tools.urlEncodeDecode.title')}</Title>
fisherdaddy's avatar
fisherdaddy committed
95
          
96 97 98 99 100 101 102 103 104 105 106
          <div className="flex flex-col gap-6">
            <div className="space-y-2">
              <label className="block text-sm font-medium text-gray-700">
                {t('tools.urlEncodeDecode.modeLabel')}
              </label>
              <select
                value={mode}
                onChange={handleModeChange}
                className="w-full sm:w-48 px-3 py-2 bg-white/80 backdrop-blur-sm border border-indigo-100 rounded-xl
                  focus:ring-4 focus:ring-indigo-100 focus:border-indigo-300 focus:outline-none
                  text-sm text-gray-700 transition duration-300"
fisherdaddy's avatar
fisherdaddy committed
107
              >
108 109 110 111 112 113 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 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163
                <option value="encode">{t('tools.urlEncodeDecode.encode')}</option>
                <option value="decode">{t('tools.urlEncodeDecode.decode')}</option>
              </select>
            </div>
            
            <div className="flex flex-col lg:flex-row gap-4 lg:gap-6">
              <div className="w-full lg:w-1/2 space-y-2">
                <label className="block text-sm font-medium text-gray-700">
                  {mode === 'decode' ? t('tools.urlDecode.inputLabel') : t('tools.urlEncode.inputLabel')}
                </label>
                <textarea
                  value={input}
                  onChange={handleInputChange}
                  placeholder={mode === 'decode' ? t('tools.urlDecode.inputLabel') : t('tools.urlEncode.inputLabel')}
                  className="w-full h-[calc(100vh-400px)] px-4 py-3 bg-white/80 backdrop-blur-sm border border-indigo-100 rounded-xl
                    focus:ring-4 focus:ring-indigo-100 focus:border-indigo-300 focus:outline-none
                    text-sm font-mono text-gray-700 transition duration-300 resize-none"
                />
              </div>

              <div className="w-full lg:w-1/2 space-y-2">
                <label className="block text-sm font-medium text-gray-700">
                  {mode === 'decode' ? t('tools.urlDecode.resultLabel') : t('tools.urlEncode.resultLabel')}
                </label>
                <div className="relative h-[calc(100vh-400px)]">
                  <div className="h-full w-full px-4 py-3 bg-white/80 backdrop-blur-sm border border-indigo-100 
                    rounded-xl text-sm font-mono text-gray-700 whitespace-pre-wrap break-all overflow-auto">
                    {resultText}
                  </div>
                  <button
                    onClick={handleCopy}
                    className={`absolute top-2 right-2 flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium rounded-lg transition-all duration-200
                      ${isCopied
                        ? 'bg-green-100 text-green-700'
                        : 'bg-white/50 hover:bg-indigo-50 text-gray-600 hover:text-indigo-600'
                      }`}
                  >
                    {isCopied ? (
                      <>
                        <svg className="w-4 h-4" viewBox="0 0 24 24" fill="currentColor">
                          <path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/>
                        </svg>
                        {t('tools.jsonFormatter.copied')}
                      </>
                    ) : (
                      <>
                        <svg className="w-4 h-4" viewBox="0 0 24 24" fill="currentColor">
                          <path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/>
                        </svg>
                        {t('tools.jsonFormatter.copy')}
                      </>
                    )}
                  </button>
                </div>
              </div>
            </div>
fisherdaddy's avatar
fisherdaddy committed
164
          </div>
165 166
        </ContentWrapper>
      </Container>
fisherdaddy's avatar
fisherdaddy committed
167 168 169 170
    </>
  );
}

fisherdaddy's avatar
fisherdaddy committed
171
export default UrlEncoderDecoder;