Files
i-tools/app/text-formatter/page.tsx
yfan 3d175d75af
Some checks failed
Build and Push Docker Image / build (push) Has been cancelled
Sync to CNB / sync (push) Has been cancelled
Delete old workflow runs / del_runs (push) Has been cancelled
Upstream Sync / Sync latest commits from upstream repo (push) Has been cancelled
first commit
2026-01-30 16:57:44 +08:00

347 lines
12 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import React, { useState, useCallback } from "react";
import { Scissors, Copy, Eraser, FileText, CheckCircle, Trash2, Paintbrush } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import { Badge } from "@/components/ui/badge";
import { toast } from "sonner";
export default function TextFormatterPage() {
const [inputText, setInputText] = useState("");
const [outputText, setOutputText] = useState("");
const [removeSpaces, setRemoveSpaces] = useState(true);
const [removeLineBreaks, setRemoveLineBreaks] = useState(true);
const [removeExtraWhitespace, setRemoveExtraWhitespace] = useState(true);
const [stats, setStats] = useState({
originalChars: 0,
originalLines: 0,
formattedChars: 0,
formattedLines: 0,
spacesRemoved: 0,
lineBreaksRemoved: 0,
});
const calculateStats = useCallback((original: string, formatted: string) => {
const originalChars = original.length;
const originalLines = original.split("\n").length;
const formattedChars = formatted.length;
const formattedLines = formatted.split("\n").length;
const originalSpaces = (original.match(/\s/g) || []).length;
const formattedSpaces = (formatted.match(/\s/g) || []).length;
const spacesRemoved = originalSpaces - formattedSpaces;
const lineBreaksRemoved = Math.max(0, originalLines - formattedLines);
setStats({
originalChars,
originalLines,
formattedChars,
formattedLines,
spacesRemoved,
lineBreaksRemoved,
});
}, []);
const formatText = useCallback(() => {
if (!inputText.trim()) {
toast.warning("请输入需要格式化的文本");
return;
}
let formatted = inputText;
if (removeLineBreaks) {
formatted = formatted.replace(/\r?\n/g, "");
}
if (removeSpaces) {
formatted = formatted.replace(/\s+/g, "");
} else if (removeExtraWhitespace) {
formatted = formatted.replace(/\s+/g, " ").trim();
}
setOutputText(formatted);
calculateStats(inputText, formatted);
toast.success("文本格式化完成");
}, [
inputText,
removeSpaces,
removeLineBreaks,
removeExtraWhitespace,
calculateStats,
]);
const copyResult = useCallback(async () => {
if (!outputText) {
toast.warning("没有可复制的内容");
return;
}
try {
await navigator.clipboard.writeText(outputText);
toast.success("已复制到剪贴板");
} catch {
toast.error("复制失败");
}
}, [outputText]);
const clearAll = useCallback(() => {
setInputText("");
setOutputText("");
setStats({
originalChars: 0,
originalLines: 0,
formattedChars: 0,
formattedLines: 0,
spacesRemoved: 0,
lineBreaksRemoved: 0,
});
}, []);
const quickClean = useCallback(() => {
if (!inputText.trim()) {
toast.warning("请输入需要格式化的文本");
return;
}
const formatted = inputText
.replace(/\r?\n/g, "")
.replace(/\t/g, "")
.replace(/\s+/g, "")
.trim();
setOutputText(formatted);
calculateStats(inputText, formatted);
toast.success("快速清理完成");
}, [inputText, calculateStats]);
const handleInputChange = useCallback((value: string) => {
setInputText(value);
}, []);
return (
<div className="space-y-6 animate-in fade-in slide-in-from-bottom-4 duration-500">
{/* Page Header */}
<div className="flex items-center space-x-4 border-b pb-4">
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-linear-to-br from-pink-500 to-rose-600 shadow-lg">
<Scissors className="h-6 w-6 text-white" />
</div>
<div>
<h1 className="text-2xl font-bold tracking-tight"></h1>
<p className="text-muted-foreground">
</p>
</div>
</div>
{/* Options Panel */}
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-base font-medium"></CardTitle>
</CardHeader>
<CardContent>
<div className="flex flex-wrap gap-6">
<div className="flex items-center space-x-2">
<Switch
id="removeSpaces"
checked={removeSpaces}
onCheckedChange={setRemoveSpaces}
/>
<Label htmlFor="removeSpaces"></Label>
</div>
<div className="flex items-center space-x-2">
<Switch
id="removeLineBreaks"
checked={removeLineBreaks}
onCheckedChange={setRemoveLineBreaks}
/>
<Label htmlFor="removeLineBreaks"></Label>
</div>
<div className="flex items-center space-x-2">
<Switch
id="removeExtraWhitespace"
checked={removeExtraWhitespace}
onCheckedChange={setRemoveExtraWhitespace}
disabled={removeSpaces}
/>
<Label htmlFor="removeExtraWhitespace" className={removeSpaces ? "text-muted-foreground" : ""}>
</Label>
</div>
</div>
</CardContent>
</Card>
{/* Action Bar */}
<Card>
<CardContent className="flex flex-wrap items-center gap-4 p-4">
<Button onClick={formatText} disabled={!inputText.trim()} className="gap-2">
<Paintbrush className="h-4 w-4" />
</Button>
<Button
variant="secondary"
onClick={quickClean}
disabled={!inputText.trim()}
className="gap-2"
>
<Scissors className="h-4 w-4" />
</Button>
<Button
variant="outline"
onClick={copyResult}
disabled={!outputText}
className="gap-2"
>
<Copy className="h-4 w-4" />
</Button>
<Button
variant="ghost"
onClick={clearAll}
className="gap-2 text-destructive hover:text-destructive/90 hover:bg-destructive/10"
>
<Eraser className="h-4 w-4" />
</Button>
</CardContent>
</Card>
{/* Stats Panel */}
{(inputText || outputText) && (
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<Card>
<CardContent className="p-4 flex flex-col items-center justify-center text-center space-y-1">
<div className="text-xs text-muted-foreground flex items-center gap-1">
<FileText className="h-3 w-3" />
</div>
<div className="text-2xl font-bold">{stats.originalChars}</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4 flex flex-col items-center justify-center text-center space-y-1">
<div className="text-xs text-muted-foreground flex items-center gap-1">
<CheckCircle className="h-3 w-3" />
</div>
<div className="text-2xl font-bold text-emerald-500">{stats.formattedChars}</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4 flex flex-col items-center justify-center text-center space-y-1">
<div className="text-xs text-muted-foreground flex items-center gap-1">
<Trash2 className="h-3 w-3" />
</div>
<div className="text-2xl font-bold text-cyan-500">{stats.spacesRemoved}</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4 flex flex-col items-center justify-center text-center space-y-1">
<div className="text-xs text-muted-foreground flex items-center gap-1">
<Scissors className="h-3 w-3" />
</div>
<div className="text-2xl font-bold text-violet-500">{stats.lineBreaksRemoved}</div>
</CardContent>
</Card>
</div>
)}
{/* Main Content */}
<div className="grid gap-6 lg:grid-cols-2">
<Card className="flex flex-col">
<CardHeader className="py-3">
<CardTitle className="text-base font-medium flex items-center justify-between">
<span></span>
{inputText.trim() && (
<Badge variant="secondary">
{stats.originalChars}
</Badge>
)}
</CardTitle>
</CardHeader>
<CardContent className="flex-1 p-0">
<Textarea
value={inputText}
onChange={(e) => handleInputChange(e.target.value)}
placeholder="请粘贴需要格式化的文本..."
className="min-h-[400px] border-0 rounded-none focus-visible:ring-0 resize-none font-mono text-sm leading-relaxed p-4"
/>
</CardContent>
</Card>
<Card className="flex flex-col">
<CardHeader className="py-3">
<CardTitle className="text-base font-medium flex items-center justify-between">
<span></span>
{outputText && (
<div className="flex items-center gap-2">
<Badge variant="default" className="bg-emerald-500 hover:bg-emerald-600">
{stats.formattedChars}
</Badge>
<Button
variant="ghost"
size="icon"
className="h-6 w-6"
onClick={copyResult}
>
<Copy className="h-3 w-3" />
</Button>
</div>
)}
</CardTitle>
</CardHeader>
<CardContent className="flex-1 p-0 bg-muted/30">
<Textarea
value={outputText}
readOnly
placeholder="格式化后的纯文本将显示在这里..."
className="min-h-[400px] border-0 rounded-none focus-visible:ring-0 resize-none font-mono text-sm leading-relaxed p-4 bg-transparent"
/>
</CardContent>
</Card>
</div>
{/* Info Card */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-base">
<span className="text-xl">💡</span> 使
</CardTitle>
</CardHeader>
<CardContent>
<div className="grid gap-6 md:grid-cols-2">
<div className="space-y-2">
<h4 className="font-semibold text-sm"></h4>
<ul className="list-disc pl-4 text-sm text-muted-foreground space-y-1">
<li><strong></strong></li>
<li><strong></strong></li>
<li><strong></strong></li>
<li><strong></strong></li>
</ul>
</div>
<div className="space-y-2">
<h4 className="font-semibold text-sm">使</h4>
<ul className="list-disc pl-4 text-sm text-muted-foreground space-y-1">
<li>WordPDF复制的文本</li>
<li></li>
<li></li>
<li></li>
</ul>
</div>
</div>
</CardContent>
</Card>
</div>
);
}