54 lines
7.3 KiB
TypeScript
54 lines
7.3 KiB
TypeScript
"use client";
|
|
|
|
import Link from "next/link";
|
|
import { ArrowLeft, Download, RefreshCw, Send } from "lucide-react";
|
|
import { FormEvent, useEffect, useState } from "react";
|
|
import { ApiError, apiFetch } from "@/lib/api";
|
|
|
|
interface HistoryItem { query_id?: string; question?: string; status?: string; row_count?: number; create_time?: string; }
|
|
|
|
export default function Nl2SqlPage() {
|
|
const [tab, setTab] = useState<"query" | "history" | "config">("query");
|
|
const [question, setQuestion] = useState("");
|
|
const [result, setResult] = useState<unknown>(null);
|
|
const [history, setHistory] = useState<HistoryItem[]>([]);
|
|
const [config, setConfig] = useState<Record<string, unknown>>({});
|
|
const [message, setMessage] = useState("");
|
|
const [loading, setLoading] = useState(false);
|
|
const [userType, setUserType] = useState<string | null>(null);
|
|
const isAdmin = userType === "ADMIN";
|
|
const canQuery = userType === "EMPLOYEE";
|
|
|
|
useEffect(() => {
|
|
void apiFetch<{ user: { user_type?: string } }>("/auth/me")
|
|
.then(({ user }) => {
|
|
const nextType = user.user_type ?? null;
|
|
setUserType(nextType);
|
|
if (nextType === "ADMIN") setTab("config");
|
|
})
|
|
.catch(() => setUserType(null));
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (tab === "history" && canQuery) void loadHistory();
|
|
if (tab === "config" && isAdmin) void loadConfig();
|
|
}, [tab, isAdmin, canQuery]);
|
|
|
|
async function query(event: FormEvent) {
|
|
event.preventDefault(); setLoading(true); setMessage("");
|
|
if (!canQuery) { setMessage("当前账号仅可管理 NL2SQL 配置,不能执行数据查询。"); setLoading(false); return; }
|
|
try {
|
|
setResult(await apiFetch("/nl2sql/query", { method: "POST", body: JSON.stringify({ question, output_format: "json", include_sql: true, page: 1, page_size: 100 }) }));
|
|
} catch (error) {
|
|
setMessage(error instanceof ApiError && error.status === 403 ? "当前账号没有 NL2SQL 查询权限,请使用员工账号登录。" : error instanceof Error ? error.message : "查询失败");
|
|
} finally { setLoading(false); }
|
|
}
|
|
|
|
async function loadHistory() { try { const response = await apiFetch<{ items?: HistoryItem[] }>("/nl2sql/query-history?page=1&page_size=50"); setHistory(response.items ?? []); } catch (error) { setMessage(error instanceof Error ? error.message : "历史加载失败"); } }
|
|
async function loadConfig() { try { setConfig(await apiFetch<Record<string, unknown>>("/nl2sql/admin/runtime-config")); } catch (error) { setMessage(error instanceof Error ? error.message : "配置加载失败"); } }
|
|
async function saveConfig() { try { await apiFetch("/nl2sql/admin/runtime-config", { method: "PATCH", body: JSON.stringify(config) }); setMessage("运行配置已保存"); } catch (error) { setMessage(error instanceof Error ? error.message : "配置保存失败"); } }
|
|
|
|
if (userType === null) return <main className="min-h-screen bg-slate-50 px-10 py-10"><div className="mx-auto max-w-6xl"><Link href="/admin/common" className="inline-flex items-center gap-2 text-sm text-slate-500"><ArrowLeft className="h-4 w-4" />返回员工工作区</Link><p className="mt-10 text-sm text-slate-500">正在校验账号权限...</p></div></main>;
|
|
return <main className="min-h-screen bg-slate-50 px-10 py-10"><div className="mx-auto max-w-6xl"><Link href="/admin/common" className="inline-flex items-center gap-2 text-sm text-slate-500"><ArrowLeft className="h-4 w-4" />返回员工工作区</Link><h1 className="mt-8 text-3xl font-semibold text-[var(--brand-navy)]">NL2SQL 数据中心</h1>{isAdmin ? <p className="mt-4 rounded-lg bg-blue-50 p-4 text-sm text-blue-700">当前为 ADMIN 账号,仅开放运行配置管理。数据查询由普通员工账号使用。</p> : <p className="mt-4 rounded-lg bg-blue-50 p-4 text-sm text-blue-700">当前账号可使用查询和历史功能,运行配置仅对 ADMIN 开放。</p>}<div className="mt-6 flex gap-2 border-b border-slate-200">{canQuery && <><button onClick={() => setTab("query")} className={`border-b-2 px-4 py-3 text-sm ${tab === "query" ? "border-[var(--brand-primary)] text-[var(--brand-primary)]" : "border-transparent text-slate-500"}`}>自然语言查询</button><button onClick={() => setTab("history")} className={`border-b-2 px-4 py-3 text-sm ${tab === "history" ? "border-[var(--brand-primary)] text-[var(--brand-primary)]" : "border-transparent text-slate-500"}`}>查询历史</button></>}{isAdmin && <button onClick={() => setTab("config")} className={`border-b-2 px-4 py-3 text-sm ${tab === "config" ? "border-[var(--brand-primary)] text-[var(--brand-primary)]" : "border-transparent text-slate-500"}`}>运行配置</button>}</div>{message && <p className="mt-4 rounded-lg bg-rose-50 p-4 text-sm text-rose-600">{message}</p>}{tab === "query" && canQuery && <><form onSubmit={query} className="mt-6 flex gap-3 rounded-xl border border-slate-200 bg-white p-5"><input required value={question} onChange={(event) => setQuestion(event.target.value)} placeholder="例如:查询本月基金产品数量" className="min-w-0 flex-1 rounded-lg border border-slate-200 px-4 py-3 text-sm" /><button disabled={loading} className="flex items-center gap-2 rounded-lg bg-[var(--brand-primary)] px-4 py-3 text-sm text-white"><Send className="h-4 w-4" />查询</button></form><pre className="mt-6 min-h-64 overflow-auto rounded-xl border border-slate-200 bg-white p-6 text-xs text-slate-700">{result ? JSON.stringify(result, null, 2) : "查询结果将在这里显示"}</pre></>}{tab === "history" && canQuery && <section className="mt-6 rounded-xl border border-slate-200 bg-white p-6"><div className="flex justify-between"><h2 className="font-semibold">查询历史</h2><button onClick={() => void loadHistory()} aria-label="刷新历史" className="text-slate-500"><RefreshCw className="h-4 w-4" /></button></div><div className="mt-4 divide-y divide-slate-100">{history.map((item) => <div key={item.query_id} className="flex items-center justify-between gap-4 py-4 text-sm"><button onClick={() => void apiFetch(`/nl2sql/query-history/${item.query_id}`).then(setResult)} className="text-left"><p>{item.question ?? item.query_id}</p><p className="mt-1 text-xs text-slate-400">{item.status} · {item.row_count ?? 0} 行 · {item.create_time ?? ""}</p></button><button onClick={() => window.open(`/backend/nl2sql/query-history/${item.query_id}/export`, "_blank")} aria-label="导出查询历史" className="text-slate-500"><Download className="h-4 w-4" /></button></div>)}{!history.length && <p className="py-8 text-sm text-slate-400">暂无查询历史</p>}</div>{Boolean(result) && <pre className="mt-5 max-h-80 overflow-auto rounded-lg bg-slate-50 p-4 text-xs">{JSON.stringify(result, null, 2)}</pre>}</section>}{tab === "config" && isAdmin && <section className="mt-6 rounded-xl border border-slate-200 bg-white p-6"><div className="flex justify-between"><h2 className="font-semibold">运行配置</h2><button onClick={() => void loadConfig()} aria-label="刷新配置" className="text-slate-500"><RefreshCw className="h-4 w-4" /></button></div><textarea value={JSON.stringify(config, null, 2)} onChange={(event) => { try { setConfig(JSON.parse(event.target.value)); } catch { /* wait for valid JSON */ } }} className="mt-5 min-h-80 w-full rounded-lg border border-slate-200 p-4 font-mono text-xs" /><button onClick={() => void saveConfig()} className="mt-4 rounded-lg bg-[var(--brand-primary)] px-4 py-2 text-sm text-white">保存配置</button></section>}</div></main>;
|
|
}
|