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

387 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, FileCode, Sparkles, Minimize2, CheckCircle2, XCircle, Upload, ChevronLeft, ChevronRight } from 'lucide-react';
import type { HtmlFormatConfig, HtmlFormatResult, HtmlValidateResult } from '@/types/html';
export function HtmlFormatterPage() {
const [input, setInput] = useState('');
const [output, setOutput] = useState('');
const [validation, setValidation] = useState<HtmlValidateResult | null>(null);
const [config, setConfig] = useState<HtmlFormatConfig>({
indent: 2,
mode: 'pretty',
});
const [copied, setCopied] = useState(false);
const [isProcessing, setIsProcessing] = useState(false);
const [isInputCollapsed, setIsInputCollapsed] = useState(false);
// 监听输入变化,自动验证
useEffect(() => {
if (input.trim()) {
validateHtml();
} else {
setValidation(null);
}
}, [input]);
// 验证 HTML
const validateHtml = useCallback(async () => {
if (!input.trim()) {
setValidation(null);
return;
}
try {
const result = await invoke<HtmlValidateResult>('validate_html', {
input,
});
setValidation(result);
} catch (error) {
console.error('验证失败:', error);
}
}, [input]);
// 格式化 HTML
const formatHtml = useCallback(async () => {
if (!input.trim()) {
return;
}
setIsProcessing(true);
try {
const result = await invoke<HtmlFormatResult>('format_html', {
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]);
// 压缩 HTML
const compactHtml = useCallback(async () => {
if (!input.trim()) {
return;
}
setIsProcessing(true);
try {
const result = await invoke<HtmlFormatResult>('compact_html', {
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 = `<!DOCTYPE html>
<html><head><title>示例</title></head><body><div class="container"><h1>欢迎</h1><p>这是一个示例。</p></div></body></html>`;
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">HTML </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> HTML </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"> HTML</CardTitle>
<CardDescription> HTML </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="在此输入 HTML..."
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>
{validation.errorLine && (
<p className="text-xs text-destructive/80 mt-1">
位置: {validation.errorLine}
</p>
)}
</div>
)}
{/* 操作按钮 */}
<div className="flex gap-2 mt-4">
<Button
onClick={formatHtml}
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={compactHtml}
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> HTML</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="html"
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. HTML </p>
<p>2. HTML </p>
<p>3. </p>
<p>4. "格式化" HTML"压缩"</p>
<p>5. "复制"</p>
</CardContent>
</Card>
</div>
</main>
</div>
);
}