Files
tauri-shadcn-vite-project/src/components/features/XmlFormatter/XmlFormatterPage.tsx
shenjianZ 910a50fa45 feat: 添加代码语法高亮功能和 HTML 格式化依赖
- 集成 react-syntax-highlighter 实现代码高亮显示
  - 新增 code-highlighter UI 组件和 syntax-helpers 工具
  - 添加 HTML/XML 格式化相关 Rust 依赖(minify-html、markup_fmt 等)
  - 在开发指南中整合 Rust-TS 跨语言命名规范
  - 移除冗余的 Tauri_Naming_Conventions.md 文档
  - 更新 Claude Code 配置添加工具命令权限
2026-02-11 09:46:49 +08:00

320 lines
12 KiB
TypeScript
Raw 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.
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>
);
}