259 lines
10 KiB
TypeScript
259 lines
10 KiB
TypeScript
|
||
import React, { useState, useEffect } from 'react';
|
||
import { generateSql } from './services/gemini';
|
||
import { Button } from './components/Button';
|
||
import { TextArea } from './components/TextArea';
|
||
import { Select } from './components/Select';
|
||
import { SettingsModal } from './components/SettingsModal';
|
||
import { LoadingState, DatabaseType } from './types';
|
||
|
||
// Default placeholders to help the user understand what to input
|
||
const PLACEHOLDER_STRUCTURE = `例如:
|
||
CREATE TABLE student (
|
||
id INT PRIMARY KEY,
|
||
name VARCHAR(50) COMMENT '姓名',
|
||
school_code VARCHAR(20) COMMENT '学校代码',
|
||
nation_code VARCHAR(10) COMMENT '国籍代码',
|
||
politics_code VARCHAR(10) COMMENT '政治面貌代码',
|
||
year VARCHAR(4) COMMENT '年度',
|
||
address VARCHAR(200) COMMENT '家庭住址'
|
||
);
|
||
|
||
CREATE TABLE school (
|
||
code VARCHAR(20),
|
||
name VARCHAR(100)
|
||
);`;
|
||
|
||
const PLACEHOLDER_DICT = `例如:
|
||
字典表 dict_common (
|
||
type_code VARCHAR(50), -- 字典类型,如 'NATION', 'POLITICS'
|
||
item_code VARCHAR(50), -- 实际值,如 '01', 'CN'
|
||
item_name VARCHAR(100) -- 显示名,如 '汉族', '中国'
|
||
)
|
||
|
||
关联关系:
|
||
- student.nation_code -> dict_common (type_code='NATION')
|
||
- student.politics_code -> dict_common (type_code='POLITICS')`;
|
||
|
||
const PLACEHOLDER_REQ = `例如:
|
||
我需要查询学校基本信息。
|
||
输出:学校名称,学校代码,学生姓名,手机号,家庭住址,年度,政治面貌(需要字典翻译),国籍(需要字典翻译)。`;
|
||
|
||
const DB_OPTIONS = [
|
||
{ value: 'MySQL', label: 'MySQL / MariaDB' },
|
||
{ value: 'PostgreSQL', label: 'PostgreSQL' },
|
||
{ value: 'Oracle', label: 'Oracle Database' },
|
||
{ value: 'SQL Server', label: 'SQL Server (MSSQL)' },
|
||
{ value: 'Hive', label: 'Hive / SparkSQL' },
|
||
{ value: 'Dm', label: '达梦数据库 (Dameng)' },
|
||
{ value: 'SQLite', label: 'SQLite' },
|
||
];
|
||
|
||
const App: React.FC = () => {
|
||
const [databaseType, setDatabaseType] = useState<DatabaseType>('MySQL');
|
||
const [tableStructure, setTableStructure] = useState<string>('');
|
||
const [dictionaryData, setDictionaryData] = useState<string>('');
|
||
const [requirement, setRequirement] = useState<string>('');
|
||
|
||
const [generatedSql, setGeneratedSql] = useState<string>('');
|
||
const [status, setStatus] = useState<LoadingState>(LoadingState.IDLE);
|
||
const [errorMsg, setErrorMsg] = useState<string | null>(null);
|
||
|
||
// API Key Management
|
||
const [apiKey, setApiKey] = useState<string>('');
|
||
const [isSettingsOpen, setIsSettingsOpen] = useState<boolean>(false);
|
||
|
||
useEffect(() => {
|
||
const storedKey = localStorage.getItem('user_api_key');
|
||
if (storedKey) {
|
||
setApiKey(storedKey);
|
||
}
|
||
}, []);
|
||
|
||
const handleSaveApiKey = (key: string) => {
|
||
setApiKey(key);
|
||
localStorage.setItem('user_api_key', key);
|
||
setIsSettingsOpen(false);
|
||
};
|
||
|
||
const handleGenerate = async () => {
|
||
if (!tableStructure.trim() || !requirement.trim()) {
|
||
setErrorMsg("请至少填写表结构和查询需求。");
|
||
return;
|
||
}
|
||
|
||
setStatus(LoadingState.LOADING);
|
||
setErrorMsg(null);
|
||
setGeneratedSql('');
|
||
|
||
try {
|
||
const sql = await generateSql({
|
||
tableStructure,
|
||
dictionaryData,
|
||
requirement,
|
||
databaseType,
|
||
apiKey // Pass the custom API key
|
||
});
|
||
setGeneratedSql(sql);
|
||
setStatus(LoadingState.SUCCESS);
|
||
} catch (err: any) {
|
||
setErrorMsg(err.message || "未知错误");
|
||
setStatus(LoadingState.ERROR);
|
||
}
|
||
};
|
||
|
||
const copyToClipboard = () => {
|
||
if (generatedSql) {
|
||
navigator.clipboard.writeText(generatedSql);
|
||
}
|
||
};
|
||
|
||
return (
|
||
<div className="flex flex-col h-screen bg-slate-50">
|
||
<SettingsModal
|
||
isOpen={isSettingsOpen}
|
||
onClose={() => setIsSettingsOpen(false)}
|
||
onSave={handleSaveApiKey}
|
||
currentApiKey={apiKey}
|
||
/>
|
||
|
||
{/* Header */}
|
||
<header className="bg-white border-b border-gray-200 px-6 py-4 flex items-center justify-between shrink-0">
|
||
<div className="flex items-center gap-3">
|
||
<div className="w-8 h-8 bg-indigo-600 rounded-lg flex items-center justify-center">
|
||
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4" />
|
||
</svg>
|
||
</div>
|
||
<div>
|
||
<h1 className="text-xl font-bold text-slate-800">SQL Translate Pro</h1>
|
||
<p className="text-xs text-slate-500">智能 SQL 生成 & 字典自动转义工具</p>
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<Button variant="outline" onClick={() => setIsSettingsOpen(true)} className="flex items-center gap-2 text-xs md:text-sm">
|
||
<svg className="w-4 h-4 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||
</svg>
|
||
设置 API Key
|
||
</Button>
|
||
</div>
|
||
</header>
|
||
|
||
{/* Main Content - Two Columns Layout */}
|
||
<main className="flex-1 overflow-hidden flex flex-col md:flex-row">
|
||
|
||
{/* Left Panel: Inputs */}
|
||
<div className="w-full md:w-1/2 lg:w-5/12 p-6 flex flex-col gap-6 overflow-y-auto border-r border-gray-200 bg-white">
|
||
|
||
<div>
|
||
<Select
|
||
label="目标数据库类型"
|
||
options={DB_OPTIONS}
|
||
value={databaseType}
|
||
onChange={(e) => setDatabaseType(e.target.value as DatabaseType)}
|
||
helperText="生成结果将适配所选数据库的方言"
|
||
/>
|
||
</div>
|
||
|
||
<div className="flex-1 min-h-[200px]">
|
||
<TextArea
|
||
label="1. 表结构与字段说明"
|
||
helperText="粘贴 DDL 或字段描述"
|
||
placeholder={PLACEHOLDER_STRUCTURE}
|
||
value={tableStructure}
|
||
onChange={(e) => setTableStructure(e.target.value)}
|
||
/>
|
||
</div>
|
||
|
||
<div className="flex-1 min-h-[150px]">
|
||
<TextArea
|
||
label="2. 字典表信息"
|
||
helperText="说明字典表结构及关联方式"
|
||
placeholder={PLACEHOLDER_DICT}
|
||
value={dictionaryData}
|
||
onChange={(e) => setDictionaryData(e.target.value)}
|
||
/>
|
||
</div>
|
||
|
||
<div className="flex-1 min-h-[120px]">
|
||
<TextArea
|
||
label="3. 最终需求"
|
||
helperText="你想要查询哪些字段?"
|
||
placeholder={PLACEHOLDER_REQ}
|
||
value={requirement}
|
||
onChange={(e) => setRequirement(e.target.value)}
|
||
/>
|
||
</div>
|
||
|
||
<div className="pt-2 sticky bottom-0 bg-white pb-2">
|
||
<Button
|
||
onClick={handleGenerate}
|
||
isLoading={status === LoadingState.LOADING}
|
||
className="w-full shadow-lg"
|
||
>
|
||
生成 SQL 查询
|
||
</Button>
|
||
{errorMsg && (
|
||
<div className="mt-2 text-red-500 text-sm bg-red-50 p-2 rounded border border-red-100">
|
||
{errorMsg}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Right Panel: Output */}
|
||
<div className="w-full md:w-1/2 lg:w-7/12 bg-slate-50 flex flex-col overflow-hidden relative">
|
||
|
||
<div className="px-6 py-4 border-b border-gray-200 bg-white flex justify-between items-center shrink-0">
|
||
<h2 className="text-lg font-semibold text-slate-700">生成结果 <span className="text-xs font-normal text-slate-400 ml-2">({databaseType})</span></h2>
|
||
{status === LoadingState.SUCCESS && (
|
||
<Button variant="outline" onClick={copyToClipboard} className="text-xs py-1.5 h-8">
|
||
复制 SQL
|
||
</Button>
|
||
)}
|
||
</div>
|
||
|
||
<div className="flex-1 p-6 overflow-y-auto">
|
||
{status === LoadingState.IDLE && (
|
||
<div className="h-full flex flex-col items-center justify-center text-slate-400">
|
||
<svg className="w-16 h-16 mb-4 opacity-50" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4" />
|
||
</svg>
|
||
<p>在左侧选择数据库类型,输入信息,点击生成</p>
|
||
</div>
|
||
)}
|
||
|
||
{status === LoadingState.LOADING && (
|
||
<div className="h-full flex flex-col items-center justify-center text-indigo-500">
|
||
<div className="animate-pulse flex flex-col items-center">
|
||
<div className="h-2.5 bg-indigo-200 rounded-full w-48 mb-4"></div>
|
||
<div className="h-2 bg-indigo-100 rounded-full w-32 mb-2.5"></div>
|
||
<div className="h-2 bg-indigo-100 rounded-full w-40"></div>
|
||
<span className="mt-6 text-sm font-medium text-slate-500">正在分析表结构并构建 {databaseType} 查询...</span>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{status === LoadingState.SUCCESS && (
|
||
<div className="relative group">
|
||
<pre className="block p-4 rounded-lg bg-slate-900 text-slate-50 font-mono text-sm leading-relaxed whitespace-pre-wrap shadow-inner border border-slate-700">
|
||
<code>{generatedSql}</code>
|
||
</pre>
|
||
</div>
|
||
)}
|
||
|
||
{status === LoadingState.ERROR && (
|
||
<div className="h-full flex items-center justify-center text-red-400">
|
||
<p>生成失败,请重试。</p>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</main>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default App; |