Files
tauri-shadcn-vite-project/src/components/features/CodeFormatter/CodeFormatterPage.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

350 lines
13 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, Code, Sparkles, CheckCircle2, XCircle, Upload, ChevronLeft, ChevronRight } from 'lucide-react';
import type { CodeFormatConfig, CodeFormatResult, CodeValidateResult, CodeLanguage } from '@/types/code';
const LANGUAGES: { value: CodeLanguage; label: string }[] = [
{ value: 'java', label: 'Java' },
{ value: 'cpp', label: 'C++' },
{ value: 'rust', label: 'Rust' },
{ value: 'python', label: 'Python' },
{ value: 'sql', label: 'SQL' },
{ value: 'javascript', label: 'JavaScript' },
{ value: 'typescript', label: 'TypeScript' },
{ value: 'html', label: 'HTML' },
{ value: 'css', label: 'CSS' },
{ value: 'json', label: 'JSON' },
{ value: 'xml', label: 'XML' },
];
export function CodeFormatterPage() {
const [input, setInput] = useState('');
const [output, setOutput] = useState('');
const [validation, setValidation] = useState<CodeValidateResult | null>(null);
const [config, setConfig] = useState<CodeFormatConfig>({
language: 'javascript',
indent: 4,
useTabs: false,
mode: 'pretty',
});
const [copied, setCopied] = useState(false);
const [isProcessing, setIsProcessing] = useState(false);
const [isInputCollapsed, setIsInputCollapsed] = useState(false);
useEffect(() => {
if (input.trim()) {
validateCode();
} else {
setValidation(null);
}
}, [input, config.language]);
const validateCode = useCallback(async () => {
if (!input.trim()) {
setValidation(null);
return;
}
try {
const result = await invoke<CodeValidateResult>('validate_code', {
input,
language: config.language,
});
setValidation(result);
} catch (error) {
console.error('验证失败:', error);
}
}, [input, config.language]);
const formatCode = useCallback(async () => {
if (!input.trim()) return;
setIsProcessing(true);
try {
const result = await invoke<CodeFormatResult>('format_code', {
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 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 examples: Record<CodeLanguage, string> = {
javascript: 'function test(){const x=1;return x*2;}',
typescript: 'function test():number{const x:number=1;return x*2;}',
java: 'public class Test{public int test(){int x=1;return x*2;}}',
cpp: 'int test(){int x=1;return x*2;}',
rust: 'fn test()->i32{let x=1;x*2}',
python: 'def test():\n\tx=1\n\treturn x*2',
sql: 'SELECT*FROM users WHERE id=1',
html: '<div><span>test</span></div>',
css: '.test{color:red;font-size:14px}',
json: '{"name":"test","value":123}',
xml: '<root><item>test</item></root>',
};
setInput(examples[config.language] || examples.javascript);
}, [config.language]);
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">
<Code className="w-6 h-6 text-primary" />
<h1 className="text-xl font-bold"></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></CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="flex flex-wrap items-center gap-4">
<label className="text-sm font-medium">:</label>
<div className="flex flex-wrap gap-1">
{LANGUAGES.map((lang) => (
<Button
key={lang.value}
size="sm"
variant={config.language === lang.value ? 'default' : 'outline'}
onClick={() => setConfig({ ...config, language: lang.value })}
>
{lang.label}
</Button>
))}
</div>
</div>
<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, 8].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">使 Tab:</label>
<Button
size="sm"
variant={config.useTabs ? 'default' : 'outline'}
onClick={() => setConfig({ ...config, useTabs: !config.useTabs })}
>
{config.useTabs ? '开启' : '关闭'}
</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"></CardTitle>
<CardDescription></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={`在此输入 ${LANGUAGES.find(l => l.value === config.language)?.label || '代码'}...`}
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={formatCode}
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>
</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></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={config.language}
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. </p>
<p>2. </p>
<p>3. 使 Tab</p>
<p>4. "格式化"</p>
<p>5. "复制"</p>
<p className="text-xs text-muted-foreground"></p>
</CardContent>
</Card>
</div>
</main>
</div>
);
}