remove frontend directory from git tracking

This commit is contained in:
2026-09-14 17:56:43 +08:00
parent 4f296eb58c
commit 6d44b8ff37
34 changed files with 0 additions and 2648 deletions
-85
View File
@@ -1,85 +0,0 @@
"use client";
import { Bot, MessageCircle, Send, X } from "lucide-react";
import { useState } from "react";
import { apiFetch, apiStream } from "@/lib/api";
interface AgentChatProps {
employeeMode?: boolean;
}
interface ChatMessage {
id: number;
role: "user" | "agent";
content: string;
}
export function AgentChat({ employeeMode = false }: AgentChatProps) {
const [open, setOpen] = useState(false);
const [input, setInput] = useState("");
const [loading, setLoading] = useState(false);
const [sessionId, setSessionId] = useState<string | null>(null);
const [messages, setMessages] = useState<ChatMessage[]>([
{ id: 1, role: "agent", content: "您好,我是华夏基金智能助手。您可以咨询基金产品、风险等级和净值信息。" },
]);
async function ensureSession() {
if (employeeMode) return "";
if (sessionId) return sessionId;
const result = await apiFetch<{ session_id: string }>("/agent/customer/session/create", { method: "POST" });
setSessionId(result.session_id);
return result.session_id;
}
async function sendMessage() {
const content = input.trim();
if (!content || loading) return;
setInput("");
setMessages((current) => [...current, { id: Date.now(), role: "user", content }]);
setLoading(true);
try {
const activeSessionId = await ensureSession();
const response = employeeMode
? await apiStream("/advisor-agent/chat/stream", { query: content })
: await apiStream("/agent/customer/chat", { session_id: activeSessionId, query: content });
setMessages((current) => [...current, { id: Date.now() + 1, role: "agent", content: response || "暂时没有找到合适的回答,请稍后再试。" }]);
} catch {
setMessages((current) => [...current, { id: Date.now() + 1, role: "agent", content: "当前服务暂时不可用,请稍后再试。" }]);
} finally {
setLoading(false);
}
}
async function closeChat() {
setOpen(false);
if (!sessionId || employeeMode) return;
try { await apiFetch("/agent/customer/session/end", { method: "POST", body: JSON.stringify({ session_id: sessionId }) }); } catch { /* session cleanup is best effort */ }
setSessionId(null);
}
return (
<div className="fixed bottom-3 right-7 z-50">
{open && (
<div role="dialog" aria-label="华夏基金智能助手" className="mb-4 flex h-[520px] w-[380px] flex-col overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-2xl">
<div className="flex items-center justify-between bg-[var(--brand-navy)] px-5 py-4 text-white">
<div className="flex items-center gap-3">
<div className="flex h-9 w-9 items-center justify-center rounded-xl bg-[var(--brand-primary)]"><Bot className="h-5 w-5" /></div>
<div><p className="text-sm font-semibold">华夏基金智能助手</p><p className="mt-0.5 text-xs text-blue-100">在线为您提供基金信息服务</p></div>
</div>
<button aria-label="关闭聊天框" onClick={() => void closeChat()} className="rounded-lg p-2 text-blue-100 transition hover:bg-white/10 hover:text-white"><X className="h-4 w-4" /></button>
</div>
<div className="flex-1 space-y-4 overflow-y-auto bg-slate-50 p-4">
{messages.map((message) => <div key={message.id} className={`flex ${message.role === "user" ? "justify-end" : "justify-start"}`}><div className={`max-w-[82%] rounded-2xl px-4 py-3 text-sm leading-6 ${message.role === "user" ? "rounded-br-md bg-[var(--brand-primary)] text-white" : "rounded-bl-md border border-slate-200 bg-white text-slate-700"}`}>{message.content}</div></div>)}
{loading && <div className="text-xs text-slate-400">正在整理信息...</div>}
</div>
<form onSubmit={(event) => { event.preventDefault(); void sendMessage(); }} className="flex gap-2 border-t border-slate-200 bg-white p-3">
<input value={input} onChange={(event) => setInput(event.target.value)} placeholder="输入您的问题" className="min-w-0 flex-1 rounded-xl border border-slate-200 px-3 py-2.5 text-sm outline-none transition focus:border-[var(--brand-primary)]" />
<button type="submit" aria-label="发送消息" className="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-[var(--brand-primary)] text-white transition hover:bg-[var(--brand-primary-dark)] disabled:cursor-not-allowed disabled:opacity-50" disabled={loading || !input.trim()}><Send className="h-4 w-4" /></button>
</form>
</div>
)}
<button aria-label={open ? "收起智能助手" : "打开智能助手"} onClick={() => setOpen((value) => !value)} className="flex h-14 w-14 items-center justify-center rounded-full bg-[var(--brand-primary)] text-white shadow-lg shadow-indigo-900/25 transition hover:-translate-y-0.5 hover:bg-[var(--brand-primary-dark)]">{open ? <X className="h-5 w-5" /> : <MessageCircle className="h-5 w-5" />}</button>
</div>
);
}
-30
View File
@@ -1,30 +0,0 @@
"use client";
import { usePathname } from "next/navigation";
import { useEffect, useState } from "react";
import { apiFetch } from "@/lib/api";
import { AgentChat } from "@/components/agent-chat";
interface CurrentUser {
user_type?: string;
employee_role?: string | null;
}
export function AgentMount() {
const pathname = usePathname();
const [employee, setEmployee] = useState(false);
useEffect(() => {
if (!pathname.startsWith("/admin")) return;
void apiFetch<{ user: CurrentUser }>("/auth/me")
.then(({ user }) => {
const advisorRole = user.employee_role === "投顾" || user.employee_role === "投顾专员" || user.employee_role === "ADMIN";
setEmployee((user.user_type === "EMPLOYEE" || user.user_type === "ADMIN") && advisorRole);
})
.catch(() => setEmployee(false));
}, [pathname]);
if (pathname.startsWith("/admin") && employee) return <AgentChat employeeMode />;
if (pathname.startsWith("/admin")) return null;
return <AgentChat />;
}
-29
View File
@@ -1,29 +0,0 @@
"use client";
import Link from "next/link";
import { ArrowUpRight, LogOut } from "lucide-react";
import { useEffect, useState } from "react";
import { clearToken, getToken } from "@/lib/auth";
const links = [["首页", "#top"], ["关于我们", "#about"], ["核心服务", "#services"], ["基金产品", "#funds"]];
export function PublicHeader() {
const [authenticated, setAuthenticated] = useState(false);
useEffect(() => {
setAuthenticated(Boolean(getToken()));
}, []);
return (
<header className="fixed inset-x-0 top-0 z-40 border-b border-white/10 bg-[var(--brand-navy)]/85 text-white backdrop-blur-xl">
<div className="mx-auto flex h-20 max-w-7xl items-center justify-between px-8">
<Link href="/#top" className="flex items-center gap-3" aria-label="华夏基金首页">
<span className="flex h-10 w-10 items-center justify-center rounded-xl bg-white text-[var(--brand-primary)] shadow-sm"><span className="text-lg font-bold">华</span></span>
<span><span className="block text-lg font-semibold tracking-wide">华夏基金</span><span className="block text-[10px] uppercase tracking-[0.25em] text-blue-100">Huaxia Fund</span></span>
</Link>
<nav className="hidden items-center gap-8 lg:flex">{links.map(([label, href]) => <Link key={href} href={href} className="text-sm text-blue-50 transition hover:text-white">{label}</Link>)}</nav>
{authenticated ? <button onClick={() => { clearToken(); setAuthenticated(false); window.location.href = "/"; }} className="flex items-center gap-1.5 rounded-lg bg-white px-4 py-2.5 text-sm font-semibold text-[var(--brand-navy)] transition hover:bg-blue-50"><LogOut className="h-4 w-4" />退出登录</button> : <div className="flex items-center gap-3"><Link href="/login" className="flex items-center gap-1.5 rounded-lg bg-white px-4 py-2.5 text-sm font-semibold text-[var(--brand-navy)] transition hover:bg-blue-50">登录 <ArrowUpRight className="h-4 w-4" /></Link></div>}
</div>
</header>
);
}
-46
View File
@@ -1,46 +0,0 @@
"use client";
import Link from "next/link";
import { Activity, AlertTriangle, BarChart3, Check, FileText, LogOut, ShieldAlert, Users } from "lucide-react";
import { useEffect, useState } from "react";
import { apiFetch } from "@/lib/api";
import { clearToken, getRoleLabel } from "@/lib/auth";
import type { UserPayload } from "@/types/api";
interface DashboardData { overview?: { total_customers?: number; signed_customers?: number; total_aum?: string }; todos?: { pending?: number } }
interface RiskAlert { id: number; customer_id: number; alert_type: string; alert_level: string; status: string }
interface WorkOrder { id: number; work_order_no: string; order_type: string; status: string; priority: string }
interface Customer { customer_id?: number; id?: number; real_name?: string; name?: string; risk_level?: string; total_aum?: string | number }
interface Todo { id: number; todo_type: string; status: string; priority: string }
interface Visit { id: number; customer_id: number; visit_type?: string; content?: string }
function Frame({ title, subtitle, role, children }: { title: string; subtitle: string; role: string; children: React.ReactNode }) {
return <main className="min-h-screen bg-slate-50 text-slate-900"><header className="flex h-20 items-center justify-between border-b border-slate-200 bg-white px-10"><div><p className="text-xs uppercase tracking-[0.2em] text-slate-400">Huaxia Fund Operations</p><h1 className="mt-1 text-xl font-semibold text-[var(--brand-navy)]">{title}</h1></div><div className="flex items-center gap-5"><span className="text-sm text-slate-500">{role}</span><button onClick={() => { clearToken(); window.location.href = "/login"; }} className="flex items-center gap-2 text-sm text-slate-500 hover:text-[var(--brand-primary)]"><LogOut className="h-4 w-4" />退出登录</button></div></header><section className="mx-auto max-w-7xl px-10 py-10"><p className="text-sm text-slate-500">{subtitle}</p>{children}</section></main>;
}
async function getMe() { return (await apiFetch<{ user: UserPayload }>("/auth/me")).user; }
function Notice({ children }: { children: React.ReactNode }) { return <div className="mt-5 rounded-xl border border-amber-200 bg-amber-50 p-4 text-sm text-amber-800">{children}</div>; }
export function AdvisorWorkspace() {
const [user, setUser] = useState<UserPayload | null>(null); const [dashboard, setDashboard] = useState<DashboardData | null>(null); const [customers, setCustomers] = useState<Customer[]>([]); const [todos, setTodos] = useState<Todo[]>([]); const [visits, setVisits] = useState<Visit[]>([]); const [report, setReport] = useState<Record<string, unknown> | null>(null); const [error, setError] = useState("");
async function load() { try { const [current, overview, customerData, todoData, visitData, personalReport] = await Promise.all([getMe(), apiFetch<DashboardData>("/advisor/dashboard"), apiFetch<{ items?: Customer[] }>("/advisor/customers?page=1&page_size=50"), apiFetch<{ items?: Todo[] }>("/advisor/todos?page=1&page_size=20"), apiFetch<{ items?: Visit[] }>("/advisor/visits?page=1&page_size=20"), apiFetch<Record<string, unknown>>("/advisor/report/personal")]); setUser(current); setDashboard(overview); setCustomers(customerData.items ?? []); setTodos(todoData.items ?? []); setVisits(visitData.items ?? []); setReport(personalReport); } catch { setError("部分投顾数据加载失败,请确认投顾权限或后端服务状态。"); } }
useEffect(() => { void load(); }, []);
async function finishTodo(id: number) { await apiFetch(`/advisor/todos/${id}/handle`, { method: "POST", body: JSON.stringify({ action: "done" }) }); await load(); }
const overview = dashboard?.overview;
return <Frame title="投顾工作区" subtitle="客户关系、投顾服务与业务待办" role={getRoleLabel(user?.employee_role)}>{error && <Notice>{error}</Notice>}<div className="mt-8 grid gap-5 md:grid-cols-4"><div className="rounded-xl border border-slate-200 bg-white p-6"><Users className="h-5 w-5 text-[var(--brand-primary)]" /><p className="mt-5 text-sm text-slate-500">名下客户</p><p className="mt-2 text-3xl font-semibold">{overview?.total_customers ?? "--"}</p></div><div className="rounded-xl border border-slate-200 bg-white p-6"><Check className="h-5 w-5 text-[var(--brand-primary)]" /><p className="mt-5 text-sm text-slate-500">签约客户</p><p className="mt-2 text-3xl font-semibold">{overview?.signed_customers ?? "--"}</p></div><div className="rounded-xl border border-slate-200 bg-white p-6"><BarChart3 className="h-5 w-5 text-[var(--brand-primary)]" /><p className="mt-5 text-sm text-slate-500">管理资产</p><p className="mt-2 text-3xl font-semibold">{overview?.total_aum ? `¥${overview.total_aum}` : "--"}</p></div><div className="rounded-xl border border-slate-200 bg-white p-6"><Activity className="h-5 w-5 text-[var(--brand-primary)]" /><p className="mt-5 text-sm text-slate-500">待办事项</p><p className="mt-2 text-3xl font-semibold">{dashboard?.todos?.pending ?? "--"}</p></div></div><div className="mt-8 grid gap-6 lg:grid-cols-2"><section className="rounded-xl border border-slate-200 bg-white p-6"><h2 className="font-semibold">客户管理</h2><div className="mt-4 divide-y divide-slate-100">{customers.slice(0, 8).map((item) => <div key={item.customer_id ?? item.id} className="flex justify-between py-3 text-sm"><span>{item.real_name ?? item.name ?? `客户 #${item.customer_id ?? item.id}`}</span><span className="text-slate-500">{item.risk_level ?? "未评估"} · {item.total_aum ?? "--"}</span></div>)}{!customers.length && <p className="py-6 text-sm text-slate-400">暂无客户数据</p>}</div></section><section className="rounded-xl border border-slate-200 bg-white p-6"><h2 className="font-semibold">待办处理</h2><div className="mt-4 space-y-3">{todos.slice(0, 6).map((todo) => <div key={todo.id} className="flex items-center justify-between rounded-lg bg-slate-50 p-3 text-sm"><span>{todo.todo_type} · {todo.priority}</span><button onClick={() => void finishTodo(todo.id)} className="text-[var(--brand-primary)]">完成</button></div>)}{!todos.length && <p className="py-6 text-sm text-slate-400">暂无待办</p>}</div></section><section className="rounded-xl border border-slate-200 bg-white p-6"><h2 className="font-semibold">回访记录</h2><div className="mt-4 space-y-3">{visits.slice(0, 5).map((visit) => <div key={visit.id} className="rounded-lg bg-slate-50 p-3 text-sm">客户 #{visit.customer_id} · {visit.visit_type ?? "回访"}<p className="mt-1 text-xs text-slate-500">{visit.content ?? "暂无内容"}</p></div>)}{!visits.length && <p className="py-6 text-sm text-slate-400">暂无回访记录</p>}</div></section><section className="rounded-xl border border-slate-200 bg-white p-6"><h2 className="font-semibold">个人绩效</h2><pre className="mt-4 max-h-48 overflow-auto rounded-lg bg-slate-50 p-3 text-xs text-slate-600">{report ? JSON.stringify(report, null, 2) : "暂无绩效数据"}</pre></section></div></Frame>;
}
export function RiskWorkspace() {
const [user, setUser] = useState<UserPayload | null>(null); const [alerts, setAlerts] = useState<RiskAlert[]>([]); const [orders, setOrders] = useState<WorkOrder[]>([]); const [error, setError] = useState("");
async function load() { try { const [current, alertData, orderData] = await Promise.all([getMe(), apiFetch<{ items?: RiskAlert[] }>("/risk/alert/list?page=1&page_size=10"), apiFetch<{ items?: WorkOrder[] }>("/work-order/list?page=1&page_size=10")]); setUser(current); setAlerts(alertData.items ?? []); setOrders(orderData.items ?? []); } catch { setError("风控数据加载失败,请确认风控权限或后端服务状态。"); } }
useEffect(() => { void load(); }, []);
async function alertAction(id: number, action: "release" | "block" | "freeze") { await apiFetch(`/risk/alert/${id}/${action}`, { method: "POST" }); await load(); }
async function orderAction(id: number, action: "claim" | "submit-review" | "review") { const body = action === "review" ? { work_order_id: id, approve: window.confirm("确认通过该工单?"), comment: "前端工作区复核" } : { work_order_id: id }; await apiFetch(`/work-order/${action}`, { method: "POST", body: JSON.stringify(body) }); await load(); }
return <Frame title="风控工作区" subtitle="风险预警、处置与工单审核" role={getRoleLabel(user?.employee_role)}>{error && <Notice>{error}</Notice>}<div className="mt-8 grid gap-5 md:grid-cols-3"><div className="rounded-xl border border-slate-200 bg-white p-6"><ShieldAlert className="h-5 w-5 text-rose-500" /><p className="mt-5 text-sm text-slate-500">当前预警</p><p className="mt-2 text-3xl font-semibold">{alerts.length}</p></div><div className="rounded-xl border border-slate-200 bg-white p-6"><AlertTriangle className="h-5 w-5 text-amber-500" /><p className="mt-5 text-sm text-slate-500">高风险事项</p><p className="mt-2 text-3xl font-semibold">{alerts.filter((item) => item.alert_level === "高").length}</p></div><div className="rounded-xl border border-slate-200 bg-white p-6"><FileText className="h-5 w-5 text-[var(--brand-primary)]" /><p className="mt-5 text-sm text-slate-500">工单数量</p><p className="mt-2 text-3xl font-semibold">{orders.length}</p></div></div><section className="mt-8 rounded-xl border border-slate-200 bg-white p-6"><h2 className="font-semibold">风险预警处置</h2><div className="mt-4 divide-y divide-slate-100">{alerts.map((alert) => <div key={alert.id} className="flex items-center justify-between gap-4 py-4 text-sm"><span>{alert.alert_type} · 客户 #{alert.customer_id}<small className="ml-3 text-slate-400">{alert.status}</small></span><div className="flex gap-2"><button onClick={() => void alertAction(alert.id, "release")} className="rounded border px-2 py-1 text-xs hover:text-emerald-600">放行</button><button onClick={() => void alertAction(alert.id, "block")} className="rounded border px-2 py-1 text-xs hover:text-rose-600">拦截</button><button onClick={() => void alertAction(alert.id, "freeze")} className="rounded border px-2 py-1 text-xs hover:text-amber-600">冻结</button></div></div>)}{!alerts.length && <p className="py-6 text-sm text-slate-400">暂无风险预警</p>}</div></section><section className="mt-8 rounded-xl border border-slate-200 bg-white p-6"><h2 className="font-semibold">业务工单</h2><div className="mt-4 divide-y divide-slate-100">{orders.map((item) => <div key={item.id} className="flex items-center justify-between gap-4 py-4 text-sm"><span>{item.work_order_no} · {item.order_type} · {item.status}</span><div className="flex gap-2"><button onClick={() => void orderAction(item.id, "claim")} className="rounded border px-2 py-1 text-xs">认领</button><button onClick={() => void orderAction(item.id, "submit-review")} className="rounded border px-2 py-1 text-xs">提交审核</button><button onClick={() => void orderAction(item.id, "review")} className="rounded border px-2 py-1 text-xs">复核</button></div></div>)}{!orders.length && <p className="py-6 text-sm text-slate-400">暂无工单</p>}</div></section></Frame>;
}
export function CommonWorkspace() {
const [user, setUser] = useState<UserPayload | null>(null); useEffect(() => { void getMe().then(setUser).catch(() => undefined); }, []);
const isAdmin = user?.user_type === "ADMIN";
return <Frame title="员工通用工作区" subtitle="基金信息、系统工具与基础运营入口" role={getRoleLabel(user?.employee_role)}><div className="mt-8 grid gap-5 md:grid-cols-3"><Link href="/products" className="rounded-xl border border-slate-200 bg-white p-6 hover:border-[var(--brand-primary)]"><FileText className="h-5 w-5 text-[var(--brand-primary)]" /><h2 className="mt-5 font-semibold">基金数据</h2><p className="mt-2 text-sm text-slate-500">查看真实基金产品和历史净值</p></Link>{isAdmin ? <Link href="/tools/nl2sql" className="rounded-xl border border-slate-200 bg-white p-6 hover:border-[var(--brand-primary)]"><Activity className="h-5 w-5 text-[var(--brand-primary)]" /><h2 className="mt-5 font-semibold">NL2SQL 配置</h2><p className="mt-2 text-sm text-slate-500">管理员专用:运行配置和查询权限管理</p></Link> : <Link href="/tools/nl2sql" className="rounded-xl border border-slate-200 bg-white p-6 hover:border-[var(--brand-primary)]"><Activity className="h-5 w-5 text-[var(--brand-primary)]" /><h2 className="mt-5 font-semibold">数据查询</h2><p className="mt-2 text-sm text-slate-500">员工自然语言查询与历史记录</p></Link>}{isAdmin && <Link href="/tools/knowledge" className="rounded-xl border border-slate-200 bg-white p-6 hover:border-[var(--brand-primary)]"><FileText className="h-5 w-5 text-[var(--brand-primary)]" /><h2 className="mt-5 font-semibold">知识库管理</h2><p className="mt-2 text-sm text-slate-500">管理员专用:文档上传、预览和删除</p></Link>}</div></Frame>;
}