- 集成 react-syntax-highlighter 实现代码高亮显示 - 新增 code-highlighter UI 组件和 syntax-helpers 工具 - 添加 HTML/XML 格式化相关 Rust 依赖(minify-html、markup_fmt 等) - 在开发指南中整合 Rust-TS 跨语言命名规范 - 移除冗余的 Tauri_Naming_Conventions.md 文档 - 更新 Claude Code 配置添加工具命令权限
320 lines
12 KiB
TypeScript
320 lines
12 KiB
TypeScript
import { useState, useCallback, useEffect } from 'react';
|
||
import { invoke } from '@tauri-apps/api/core';
|
||
import { Button } from '@/components/ui/button';
|
||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||
import { Badge } from '@/components/ui/badge';
|
||
import { CodeHighlighter } from '@/components/ui/code-highlighter';
|
||
import { Copy, Check, FileCode, Sparkles, Minimize2, CheckCircle2, XCircle, Upload, ChevronLeft, ChevronRight } from 'lucide-react';
|
||
import type { XmlFormatConfig, XmlFormatResult, XmlValidateResult } from '@/types/xml';
|
||
|
||
export function XmlFormatterPage() {
|
||
const [input, setInput] = useState('');
|
||
const [output, setOutput] = useState('');
|
||
const [validation, setValidation] = useState<XmlValidateResult | null>(null);
|
||
const [config, setConfig] = useState<XmlFormatConfig>({
|
||
indent: 2,
|
||
mode: 'pretty',
|
||
});
|
||
const [copied, setCopied] = useState(false);
|
||
const [isProcessing, setIsProcessing] = useState(false);
|
||
const [isInputCollapsed, setIsInputCollapsed] = useState(false);
|
||
|
||
useEffect(() => {
|
||
if (input.trim()) {
|
||
validateXml();
|
||
} else {
|
||
setValidation(null);
|
||
}
|
||
}, [input]);
|
||
|
||
const validateXml = useCallback(async () => {
|
||
if (!input.trim()) {
|
||
setValidation(null);
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const result = await invoke<XmlValidateResult>('validate_xml', { input });
|
||
setValidation(result);
|
||
} catch (error) {
|
||
console.error('验证失败:', error);
|
||
}
|
||
}, [input]);
|
||
|
||
const formatXml = useCallback(async () => {
|
||
if (!input.trim()) return;
|
||
|
||
setIsProcessing(true);
|
||
try {
|
||
const result = await invoke<XmlFormatResult>('format_xml', { input, config });
|
||
if (result.success) {
|
||
setOutput(result.result);
|
||
// 格式化成功后自动收起输入区域
|
||
setIsInputCollapsed(true);
|
||
} else {
|
||
setOutput(result.error || '格式化失败');
|
||
}
|
||
} catch (error) {
|
||
console.error('格式化失败:', error);
|
||
setOutput('错误: ' + String(error));
|
||
} finally {
|
||
setIsProcessing(false);
|
||
}
|
||
}, [input, config]);
|
||
|
||
const compactXml = useCallback(async () => {
|
||
if (!input.trim()) return;
|
||
|
||
setIsProcessing(true);
|
||
try {
|
||
const result = await invoke<XmlFormatResult>('compact_xml', { input });
|
||
if (result.success) {
|
||
setOutput(result.result);
|
||
} else {
|
||
setOutput(result.error || '压缩失败');
|
||
}
|
||
} catch (error) {
|
||
console.error('压缩失败:', error);
|
||
setOutput('错误: ' + String(error));
|
||
} finally {
|
||
setIsProcessing(false);
|
||
}
|
||
}, [input]);
|
||
|
||
const copyToClipboard = useCallback(async () => {
|
||
if (!output) return;
|
||
try {
|
||
await navigator.clipboard.writeText(output);
|
||
setCopied(true);
|
||
setTimeout(() => setCopied(false), 2000);
|
||
} catch (error) {
|
||
console.error('复制失败:', error);
|
||
}
|
||
}, [output]);
|
||
|
||
const clearInput = useCallback(() => {
|
||
setInput('');
|
||
setOutput('');
|
||
setValidation(null);
|
||
}, []);
|
||
|
||
const loadExample = useCallback(() => {
|
||
const example = '<?xml version="1.0" encoding="UTF-8"?><root><item id="1"><name>示例</name><value>测试</value></item></root>';
|
||
setInput(example);
|
||
}, []);
|
||
|
||
return (
|
||
<div className="flex flex-col h-screen bg-background">
|
||
<header className="border-b bg-background/95 backdrop-blur flex-shrink-0">
|
||
<div className="container mx-auto px-4 py-4">
|
||
<div className="flex items-center gap-3">
|
||
<Button variant="ghost" size="sm" onClick={() => window.history.back()}>
|
||
← 返回
|
||
</Button>
|
||
<div className="flex items-center gap-2">
|
||
<FileCode className="w-6 h-6 text-primary" />
|
||
<h1 className="text-xl font-bold">XML 格式化工具</h1>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</header>
|
||
|
||
<main className="flex-1 container mx-auto px-4 py-6 overflow-y-auto">
|
||
<div className="max-w-6xl mx-auto space-y-6">
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle className="text-lg">配置选项</CardTitle>
|
||
<CardDescription>自定义 XML 格式化行为</CardDescription>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<div className="flex flex-wrap items-center gap-6">
|
||
<div className="flex items-center gap-3">
|
||
<label className="text-sm font-medium">缩进:</label>
|
||
<div className="flex gap-1">
|
||
{[2, 4].map((spaces) => (
|
||
<Button
|
||
key={spaces}
|
||
size="sm"
|
||
variant={config.indent === spaces ? 'default' : 'outline'}
|
||
onClick={() => setConfig({ ...config, indent: spaces })}
|
||
>
|
||
{spaces} 空格
|
||
</Button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
<div className="flex items-center gap-3">
|
||
<label className="text-sm font-medium">模式:</label>
|
||
<div className="flex gap-1">
|
||
{(['pretty', 'compact'] as const).map((mode) => (
|
||
<Button
|
||
key={mode}
|
||
size="sm"
|
||
variant={config.mode === mode ? 'default' : 'outline'}
|
||
onClick={() => setConfig({ ...config, mode })}
|
||
>
|
||
{mode === 'pretty' ? '美化' : '压缩'}
|
||
</Button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 relative">
|
||
{/* 收起/展开切换按钮 */}
|
||
<Button
|
||
variant="outline"
|
||
size="sm"
|
||
onClick={() => setIsInputCollapsed(!isInputCollapsed)}
|
||
className="absolute left-1/2 -translate-x-1/2 -top-3 z-10 gap-1 shadow-md"
|
||
>
|
||
{isInputCollapsed ? (
|
||
<>
|
||
<ChevronRight className="w-4 h-4" />
|
||
显示输入
|
||
</>
|
||
) : (
|
||
<>
|
||
<ChevronLeft className="w-4 h-4" />
|
||
隐藏输入
|
||
</>
|
||
)}
|
||
</Button>
|
||
|
||
{!isInputCollapsed && (
|
||
<Card>
|
||
<CardHeader>
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<CardTitle className="text-lg">输入 XML</CardTitle>
|
||
<CardDescription>粘贴或输入 XML 数据</CardDescription>
|
||
</div>
|
||
<div className="flex gap-2">
|
||
<Button size="sm" variant="outline" onClick={loadExample}>
|
||
<Upload className="w-4 h-4 mr-1" />
|
||
示例
|
||
</Button>
|
||
{input && (
|
||
<Button size="sm" variant="ghost" onClick={clearInput}>
|
||
清空
|
||
</Button>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<div className="relative">
|
||
<textarea
|
||
value={input}
|
||
onChange={(e) => setInput(e.target.value)}
|
||
className="w-full h-96 p-4 font-mono text-sm bg-muted rounded-lg resize-none focus:outline-none focus:ring-2 focus:ring-primary"
|
||
placeholder="在此输入 XML..."
|
||
spellCheck={false}
|
||
/>
|
||
{validation && (
|
||
<div className="absolute top-2 right-2">
|
||
{validation.isValid ? (
|
||
<Badge variant="default" className="gap-1 bg-green-500 hover:bg-green-600">
|
||
<CheckCircle2 className="w-3 h-3" />
|
||
有效
|
||
</Badge>
|
||
) : (
|
||
<Badge variant="destructive" className="gap-1">
|
||
<XCircle className="w-3 h-3" />
|
||
无效
|
||
</Badge>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
{validation && !validation.isValid && validation.errorMessage && (
|
||
<div className="mt-3 p-3 bg-destructive/10 border border-destructive/20 rounded-lg">
|
||
<p className="text-sm text-destructive font-medium">{validation.errorMessage}</p>
|
||
</div>
|
||
)}
|
||
<div className="flex gap-2 mt-4">
|
||
<Button onClick={formatXml} disabled={!input.trim() || isProcessing} className="flex-1 gap-2">
|
||
{isProcessing ? (
|
||
<>
|
||
<Sparkles className="w-4 h-4 animate-spin" />
|
||
处理中...
|
||
</>
|
||
) : (
|
||
<>
|
||
<Sparkles className="w-4 h-4" />
|
||
格式化
|
||
</>
|
||
)}
|
||
</Button>
|
||
<Button onClick={compactXml} variant="outline" disabled={!input.trim() || isProcessing}>
|
||
<Minimize2 className="w-4 h-4 mr-1" />
|
||
压缩
|
||
</Button>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
)}
|
||
|
||
<Card className={isInputCollapsed ? 'lg:col-span-2' : ''}>
|
||
<CardHeader>
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<CardTitle className="text-lg">格式化结果</CardTitle>
|
||
<CardDescription>格式化后的 XML</CardDescription>
|
||
</div>
|
||
{output && (
|
||
<Button size="sm" variant="outline" onClick={copyToClipboard} className="gap-2">
|
||
{copied ? (
|
||
<>
|
||
<Check className="w-4 h-4" />
|
||
已复制
|
||
</>
|
||
) : (
|
||
<>
|
||
<Copy className="w-4 h-4" />
|
||
复制
|
||
</>
|
||
)}
|
||
</Button>
|
||
)}
|
||
</div>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<CodeHighlighter
|
||
code={output}
|
||
language="xml"
|
||
className="w-full"
|
||
maxHeight="24rem"
|
||
showLineNumbers={true}
|
||
wrapLongLines={false}
|
||
/>
|
||
{output && (
|
||
<div className="flex gap-4 mt-4 text-sm text-muted-foreground">
|
||
<span>字符数: {output.length}</span>
|
||
<span>行数: {output.split('\n').length}</span>
|
||
</div>
|
||
)}
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle>使用说明</CardTitle>
|
||
</CardHeader>
|
||
<CardContent className="space-y-2 text-sm text-muted-foreground">
|
||
<p>1. 在左侧输入框中粘贴或输入 XML 数据</p>
|
||
<p>2. 工具会自动验证 XML 有效性,右上角显示验证状态</p>
|
||
<p>3. 选择配置选项:缩进空格数、格式化模式</p>
|
||
<p>4. 点击"格式化"按钮美化 XML,或"压缩"按钮去除空格</p>
|
||
<p>5. 点击"复制"按钮将结果复制到剪贴板</p>
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
</main>
|
||
</div>
|
||
);
|
||
}
|