Files
jisuxiang-zy/src/app/tools/url_encoder/page.tsx
T
2025-04-10 18:06:45 +08:00

398 lines
15 KiB
TypeScript

'use client';
import React, { useState, useEffect } from 'react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faExchangeAlt, faCopy, faCheck, faEraser, faHistory } from '@fortawesome/free-solid-svg-icons';
import ToolHeader from '@/components/ToolHeader';
import BackToTop from '@/components/BackToTop';
import tools from '@/config/tools';
import { useLanguage } from '@/context/LanguageContext';
// 添加CSS变量样式
const styles = {
card: "card p-6",
container: "min-h-screen flex flex-col max-w-[1440px] mx-auto p-4 md:p-6",
textarea: "w-full p-3 bg-block border border-purple-glow rounded-lg text-primary focus:border-purple focus:outline-none focus:ring-1 focus:ring-purple transition-all",
label: "text-sm text-secondary font-medium",
secondaryText: "text-sm text-tertiary",
errorBox: "p-3 bg-red-900/20 border border-red-700/30 rounded-lg text-error",
successBox: "p-3 bg-green-900/20 border border-green-700/30 rounded-lg text-success",
iconButton: "p-1 text-secondary hover:text-primary disabled:opacity-50 disabled:cursor-not-allowed",
directionIndicator: (active: boolean) => active ? "text-primary" : "text-tertiary",
historyItem: "flex px-3 py-2 text-sm rounded-lg cursor-pointer text-secondary hover:bg-block-hover transition-colors",
noHistoryText: "text-sm text-tertiary py-2 text-center italic",
swapButton: "bg-block p-2 rounded-full hover:bg-block-hover transition-colors",
encodeLabel: (active: boolean) => `text-sm ${active ? 'text-purple' : 'text-tertiary'} cursor-pointer`,
clearButton: "btn-secondary flex items-center gap-2",
copyButton: "btn-primary flex items-center gap-2",
historyButton: "btn-secondary flex items-center gap-2",
}
// 类型定义
type Direction = 'encode' | 'decode';
type EncodeMode = 'url' | 'component';
// 历史记录项目接口
interface HistoryItem {
id: string;
input: string;
output: string;
direction: Direction;
mode: EncodeMode;
timestamp: number;
}
export default function UrlEncoder() {
const { t } = useLanguage();
// 从工具配置中获取当前工具信息
const toolConfig = tools.find(tool => tool.code === 'url_encoder');
// 状态管理
const [inputText, setInputText] = useState('');
const [outputText, setOutputText] = useState('');
const [direction, setDirection] = useState<Direction>('encode');
const [encodeMode, setEncodeMode] = useState<EncodeMode>('url');
const [error, setError] = useState('');
const [success, setSuccess] = useState('');
const [copied, setCopied] = useState(false);
const [history, setHistory] = useState<HistoryItem[]>([]);
const [showHistory, setShowHistory] = useState(false);
// 从localStorage加载历史记录
useEffect(() => {
const savedHistory = localStorage.getItem('urlEncoderHistory');
if (savedHistory) {
try {
setHistory(JSON.parse(savedHistory));
} catch (e) {
console.error(t('tools.url_encoder.load_history_error'), e);
}
}
}, [t]);
// 保存历史记录到localStorage
useEffect(() => {
if (history.length > 0) {
localStorage.setItem('urlEncoderHistory', JSON.stringify(history));
}
}, [history]);
// 转换文本
useEffect(() => {
if (!inputText.trim()) {
setOutputText('');
setError('');
return;
}
try {
let result = '';
if (direction === 'encode') {
result = encodeMode === 'url'
? encodeURI(inputText)
: encodeURIComponent(inputText);
} else {
result = encodeMode === 'url'
? decodeURI(inputText)
: decodeURIComponent(inputText);
}
setOutputText(result);
setError('');
} catch (err) {
if (err instanceof Error) {
setError(err.message);
} else {
setError(t('tools.url_encoder.error_processing'));
}
setOutputText('');
}
}, [inputText, direction, encodeMode, t]);
// 反转转换方向
const swapDirection = () => {
setDirection(prev => prev === 'encode' ? 'decode' : 'encode');
setInputText(outputText);
setOutputText('');
setError('');
};
// 复制输出结果
const copyOutput = () => {
if (!outputText) return;
navigator.clipboard.writeText(outputText)
.then(() => {
setCopied(true);
setSuccess(t('tools.url_encoder.copied_to_clipboard'));
// 添加到历史记录
if (inputText.trim() && outputText.trim()) {
const newItem: HistoryItem = {
id: Date.now().toString(),
input: inputText,
output: outputText,
direction,
mode: encodeMode,
timestamp: Date.now()
};
setHistory(prev => [newItem, ...prev.slice(0, 9)]); // 保留最近10条记录
}
setTimeout(() => {
setCopied(false);
setSuccess('');
}, 2000);
})
.catch(err => {
console.error(t('tools.url_encoder.copy_failed'), err);
setError(t('tools.url_encoder.copy_failed'));
});
};
// 清空输入输出
const clearAll = () => {
setInputText('');
setOutputText('');
setError('');
setSuccess('');
};
// 从历史记录中恢复
const restoreFromHistory = (item: HistoryItem) => {
setInputText(item.input);
setDirection(item.direction);
setEncodeMode(item.mode);
setShowHistory(false);
};
// 清空历史记录
const clearHistory = () => {
setHistory([]);
localStorage.removeItem('urlEncoderHistory');
};
// 格式化时间
const formatTime = (timestamp: number) => {
const date = new Date(timestamp);
return date.toLocaleString();
};
return (
<div className={styles.container}>
{/* 工具头部 */}
<ToolHeader
toolCode="url_encoder"
icon={toolConfig?.icon || faExchangeAlt}
title=""
description=""
/>
{/* 主内容区 */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* 左侧面板 - 输入区域 */}
<div className="lg:col-span-2 space-y-6">
{/* 转换选项 */}
<div className={styles.card}>
<div className="flex flex-col md:flex-row items-center justify-center space-y-4 md:space-y-0 md:space-x-6 mb-2">
{/* 转换方向选择器 */}
<div className="flex items-center space-x-4">
<span className={styles.directionIndicator(direction === 'encode')}>
{t('tools.url_encoder.original_text')}
</span>
<button onClick={swapDirection} className={styles.swapButton}>
<FontAwesomeIcon
icon={faExchangeAlt}
className={`${direction === 'encode' ? 'text-purple' : 'text-purple rotate-180'} transition-transform`}
/>
</button>
<span className={styles.directionIndicator(direction === 'decode')}>
{t('tools.url_encoder.url_encoded_text')}
</span>
</div>
{/* 编码模式选择器 */}
<div className="flex items-center space-x-4">
<label className={styles.encodeLabel(encodeMode === 'url')}>
<input
type="radio"
name="encodeMode"
value="url"
checked={encodeMode === 'url'}
onChange={() => setEncodeMode('url')}
className="sr-only"
/>
<span>{t('tools.url_encoder.uri_encoding')}</span>
</label>
<label className={styles.encodeLabel(encodeMode === 'component')}>
<input
type="radio"
name="encodeMode"
value="component"
checked={encodeMode === 'component'}
onChange={() => setEncodeMode('component')}
className="sr-only"
/>
<span>{t('tools.url_encoder.uri_component_encoding')}</span>
</label>
</div>
</div>
<div className="mt-4 text-sm text-tertiary">
<p>
{encodeMode === 'url'
? t('tools.url_encoder.uri_encoding_description')
: t('tools.url_encoder.uri_component_description')}
</p>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* 输入文本区域 */}
<div className="space-y-3">
<label className={styles.label}>
{direction === 'encode' ? t('tools.url_encoder.input_original_text') : t('tools.url_encoder.input_encoded_text')}
</label>
<textarea
className={styles.textarea}
value={inputText}
onChange={(e) => setInputText(e.target.value)}
placeholder={direction === 'encode' ? t('tools.url_encoder.enter_text_to_encode') : t('tools.url_encoder.enter_text_to_decode')}
rows={12}
/>
<div className="flex justify-between">
<button
onClick={clearAll}
disabled={!inputText}
className={styles.clearButton}
>
<FontAwesomeIcon icon={faEraser} />
{t('tools.url_encoder.clear')}
</button>
<button
onClick={() => setShowHistory(!showHistory)}
className={styles.historyButton}
>
<FontAwesomeIcon icon={faHistory} />
{showHistory ? t('tools.url_encoder.hide_history') : t('tools.url_encoder.show_history')}
</button>
</div>
</div>
{/* 输出文本区域 */}
<div className="space-y-3">
<label className={styles.label}>
{direction === 'encode' ? t('tools.url_encoder.encoding_result') : t('tools.url_encoder.decoding_result')}
</label>
<textarea
className={styles.textarea}
value={outputText}
readOnly
placeholder={direction === 'encode' ? t('tools.url_encoder.encoded_result_placeholder') : t('tools.url_encoder.decoded_result_placeholder')}
rows={12}
/>
<div className="flex justify-end">
<button
onClick={copyOutput}
disabled={!outputText}
className={styles.copyButton}
>
<FontAwesomeIcon icon={copied ? faCheck : faCopy} />
{copied ? t('tools.url_encoder.copied') : t('tools.url_encoder.copy_result')}
</button>
</div>
{/* 消息区域 */}
{error && <div className={styles.errorBox}>{error}</div>}
{success && <div className={styles.successBox}>{success}</div>}
</div>
</div>
</div>
{/* 右侧面板 - 历史记录 */}
<div className="space-y-6">
<div className={styles.card}>
<div className="flex justify-between items-center mb-4">
<h2 className="font-medium text-primary">{t('tools.url_encoder.history')}</h2>
{history.length > 0 && (
<button
onClick={clearHistory}
className="text-sm text-secondary hover:text-primary transition-colors"
>
{t('tools.url_encoder.clear_history')}
</button>
)}
</div>
{history.length > 0 ? (
<div className="space-y-2 max-h-[500px] overflow-y-auto">
{history.map(item => (
<div
key={item.id}
className={styles.historyItem}
onClick={() => restoreFromHistory(item)}
>
<div className="flex-1 truncate">
<div className="font-medium mb-1 truncate">{item.input.substring(0, 30)}{item.input.length > 30 ? '...' : ''}</div>
<div className="flex items-center justify-between">
<span className="text-xs text-tertiary">
{item.direction === 'encode' ? t('tools.url_encoder.encode') : t('tools.url_encoder.decode')}
{item.mode === 'url' ? ' URI' : ' URI Component'}
</span>
<span className="text-xs text-tertiary">{formatTime(item.timestamp)}</span>
</div>
</div>
</div>
))}
</div>
) : (
<div className={styles.noHistoryText}>
{t('tools.url_encoder.no_history')}
</div>
)}
</div>
{/* 说明卡片 */}
<div className={styles.card}>
<h2 className="font-medium text-primary mb-4">{t('tools.url_encoder.url_encoding_explanation')}</h2>
<div className="space-y-4 text-sm text-tertiary">
<div>
<h3 className="font-medium text-secondary mb-2">{t('tools.url_encoder.uri_vs_component')}</h3>
<ul className="space-y-2">
<li><span className="text-primary">encodeURI</span>: {t('tools.url_encoder.encode_uri_description')}</li>
<li><span className="text-primary">encodeURIComponent</span>: {t('tools.url_encoder.encode_uri_component_description')}</li>
</ul>
</div>
<div>
<h3 className="font-medium text-secondary mb-2">{t('tools.url_encoder.usage_scenarios')}</h3>
<ul className="space-y-2">
<li>{t('tools.url_encoder.scenario_1')}</li>
<li>{t('tools.url_encoder.scenario_2')}</li>
<li>{t('tools.url_encoder.scenario_3')}</li>
</ul>
</div>
<div>
<h3 className="font-medium text-secondary mb-2">{t('tools.url_encoder.encoding_rules')}</h3>
<p>{t('tools.url_encoder.rules_1')}</p>
<p>{t('tools.url_encoder.rules_2')}</p>
</div>
</div>
</div>
</div>
</div>
{/* 回到顶部按钮 */}
<BackToTop />
</div>
);
}