feat: Establish core application structure, authentication, and dynamic entity data management.
This commit is contained in:
+8
-1
@@ -1,7 +1,14 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
/* config options here */
|
||||
async rewrites() {
|
||||
return [
|
||||
{
|
||||
source: '/api/:path*',
|
||||
destination: 'http://localhost:8080/api/:path*',
|
||||
},
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
|
||||
+119
-100
@@ -19,6 +19,7 @@ import {
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import DynamicForm from "@/components/DynamicForm";
|
||||
|
||||
interface DataPageProps {
|
||||
params: Promise<{
|
||||
@@ -27,7 +28,6 @@ interface DataPageProps {
|
||||
}
|
||||
|
||||
export default function EntityDataPage({ params: paramsPromise }: DataPageProps) {
|
||||
// Use React.use() to unwrap Next.js 15+ dynamic params Promise
|
||||
const params = use(paramsPromise);
|
||||
const { entityCode } = params;
|
||||
|
||||
@@ -37,13 +37,17 @@ export default function EntityDataPage({ params: paramsPromise }: DataPageProps)
|
||||
const [sorting, setSorting] = useState<SortingState>([]);
|
||||
const [columns, setColumns] = useState<ColumnDef<any>[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
async function loadData() {
|
||||
// Modal 状态
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [editingData, setEditingData] = useState<any>(null);
|
||||
|
||||
const loadData = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const userId = localStorage.getItem("crm_user_id") || "anonymous";
|
||||
const [entityRes, dataRes] = await Promise.all([
|
||||
fetch(`http://localhost:8080/api/entities/code/${entityCode}`),
|
||||
fetch(`http://localhost:8080/api/dynamic/code/${entityCode}`)
|
||||
fetch(`/api/entities/code/${entityCode}`, { headers: { 'X-Creator-Id': userId } }),
|
||||
fetch(`/api/dynamic/code/${entityCode}`, { headers: { 'X-Creator-Id': userId } })
|
||||
]);
|
||||
|
||||
if (entityRes.ok && dataRes.ok) {
|
||||
@@ -51,7 +55,6 @@ export default function EntityDataPage({ params: paramsPromise }: DataPageProps)
|
||||
const records = await dataRes.json();
|
||||
setEntity(entityData);
|
||||
|
||||
// Map backend entities to flat row format
|
||||
const mappedData = records.map((r: any) => {
|
||||
const parsedData = (typeof r.data === 'string') ? JSON.parse(r.data) : r.data;
|
||||
return {
|
||||
@@ -62,11 +65,8 @@ export default function EntityDataPage({ params: paramsPromise }: DataPageProps)
|
||||
});
|
||||
setData(mappedData);
|
||||
|
||||
// Build generic columns directly from Schema
|
||||
const dynamicColumns: ColumnDef<any>[] = [];
|
||||
|
||||
if (entityData && entityData.schemaDefinition) {
|
||||
try {
|
||||
const schema = JSON.parse(entityData.schemaDefinition);
|
||||
if (schema.properties) {
|
||||
Object.keys(schema.properties).forEach((key) => {
|
||||
@@ -75,40 +75,78 @@ export default function EntityDataPage({ params: paramsPromise }: DataPageProps)
|
||||
accessorKey: key,
|
||||
header: prop.title || key,
|
||||
cell: ({ row }) => {
|
||||
const cellValue = row.getValue(key);
|
||||
return cellValue !== undefined && cellValue !== null ? String(cellValue) : "-";
|
||||
const val = row.getValue(key);
|
||||
return val !== undefined && val !== null ? String(val) : "-";
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
} catch(e) {
|
||||
console.error("Schema Parsing Error:", e);
|
||||
}
|
||||
}
|
||||
|
||||
// System column created at
|
||||
// 添加操作列
|
||||
dynamicColumns.push({
|
||||
accessorKey: "_createdAt",
|
||||
header: "创建时间",
|
||||
cell: ({ row }) => {
|
||||
const val: string = row.getValue("_createdAt");
|
||||
return val ? new Date(val).toLocaleString() : "-";
|
||||
}
|
||||
id: "actions",
|
||||
header: "操作",
|
||||
cell: ({ row }) => (
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-indigo-600 hover:text-indigo-700 hover:bg-indigo-50 font-bold"
|
||||
onClick={() => handleEdit(row.original)}
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-rose-600 hover:text-rose-700 hover:bg-rose-50 font-bold"
|
||||
onClick={() => handleDelete(row.original._id)}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
});
|
||||
|
||||
setColumns(dynamicColumns);
|
||||
} else {
|
||||
console.error("Failed to fetch entity or data");
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Error loading table data:", err);
|
||||
console.error("Load error:", err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, [entityCode]);
|
||||
|
||||
const handleDelete = async (id: string) => {
|
||||
if (!confirm("确定要物理删除这条数据吗?该操作不可撤销,将由 AI 执行。")) return;
|
||||
try {
|
||||
const userId = localStorage.getItem("crm_user_id") || "anonymous";
|
||||
const res = await fetch(`/api/agent/chat?sessionId=${userId}_OP`, {
|
||||
method: 'POST',
|
||||
headers: { 'X-Creator-Id': userId },
|
||||
body: `请执行核心物理删除指令:删除记录 ID 为 ${id} 的数据。`
|
||||
});
|
||||
if (res.ok) {
|
||||
alert("✅ AI 已同步执行删除操作。");
|
||||
loadData();
|
||||
}
|
||||
} catch (err) {
|
||||
alert("删除失败");
|
||||
}
|
||||
};
|
||||
|
||||
const handleEdit = (row: any) => {
|
||||
// 排除系统字段
|
||||
const { _id, _createdAt, ...businessData } = row;
|
||||
setEditingData(businessData);
|
||||
setIsModalOpen(true);
|
||||
};
|
||||
|
||||
const table = useReactTable({
|
||||
data,
|
||||
columns,
|
||||
@@ -116,109 +154,90 @@ export default function EntityDataPage({ params: paramsPromise }: DataPageProps)
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
onSortingChange: setSorting,
|
||||
state: {
|
||||
sorting,
|
||||
},
|
||||
state: { sorting },
|
||||
});
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center bg-slate-50">
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<span className="animate-spin text-4xl text-indigo-500">❖</span>
|
||||
<p className="text-slate-500 font-bold tracking-widest animate-pulse">LOADING DYNAMIC RECORDS</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
if (loading && !data.length) {
|
||||
return <div className="flex h-full w-full items-center justify-center">正在加载...</div>;
|
||||
}
|
||||
|
||||
if (!entity) return <div className="p-8 text-rose-500 font-bold">实体配置未找到或服务不可用。</div>;
|
||||
|
||||
return (
|
||||
<div className="p-8 md:p-12 max-w-7xl mx-auto flex flex-col h-full gap-8 max-h-screen">
|
||||
<div className="flex flex-col gap-2 border-b border-slate-200 pb-6 shrink-0">
|
||||
<div className="p-8 md:p-12 max-w-7xl mx-auto flex flex-col h-full gap-8 relative">
|
||||
<div className="flex items-center justify-between border-b pb-8">
|
||||
<div className="flex flex-col gap-2">
|
||||
<h1 className="text-4xl font-black text-slate-800 tracking-tight flex items-center gap-3">
|
||||
<span className="p-3 bg-gradient-to-br from-indigo-50 to-indigo-100/50 text-indigo-600 rounded-2xl shadow-sm border border-indigo-100">📋</span>
|
||||
{entity.entityName}
|
||||
<span className="p-3 bg-indigo-50 text-indigo-600 rounded-2xl">📋</span>
|
||||
{entity?.entityName}
|
||||
</h1>
|
||||
<p className="text-slate-500 font-medium">模块标识: <code className="bg-slate-100 border border-slate-200 px-2 py-0.5 rounded text-sm text-slate-700 mx-2">{entity.entityCode}</code> <span className="text-sm">系统自动映射的 JSONB 动态持久化视图</span></p>
|
||||
<p className="text-slate-500 font-medium">业务代码: {entityCode}</p>
|
||||
</div>
|
||||
<Button
|
||||
className="bg-indigo-600 hover:bg-indigo-700 text-white font-black px-8 py-6 rounded-2xl shadow-xl shadow-indigo-200"
|
||||
onClick={() => { setEditingData(null); setIsModalOpen(true); }}
|
||||
>
|
||||
+ 新增业务记录
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="rounded-2xl border border-slate-200 bg-white shadow-xl shadow-slate-200/40 flex-1 flex flex-col overflow-hidden relative">
|
||||
<div className="flex-1 overflow-auto bg-slate-50/30">
|
||||
{/* 数据表格 */}
|
||||
<div className="rounded-3xl border border-slate-200 bg-white shadow-2xl overflow-hidden flex flex-col flex-1">
|
||||
<div className="overflow-auto flex-1">
|
||||
<Table>
|
||||
<TableHeader className="bg-slate-100/80 sticky top-0 z-10 backdrop-blur-md shadow-sm">
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id} className="border-b-slate-200 hover:bg-transparent">
|
||||
{headerGroup.headers.map((header) => (
|
||||
<TableHead key={header.id} className="font-bold text-slate-600 whitespace-nowrap h-12 uppercase text-xs tracking-wider">
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext()
|
||||
)}
|
||||
<TableHeader className="bg-slate-50 sticky top-0 z-10">
|
||||
{table.getHeaderGroups().map(hg => (
|
||||
<TableRow key={hg.id}>
|
||||
{hg.headers.map(h => (
|
||||
<TableHead key={h.id} className="font-bold text-slate-600 uppercase text-xs p-6">
|
||||
{flexRender(h.column.columnDef.header, h.getContext())}
|
||||
</TableHead>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{table.getRowModel().rows?.length ? (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow
|
||||
key={row.id}
|
||||
data-state={row.getIsSelected() && "selected"}
|
||||
className="hover:bg-indigo-50/50 border-b-slate-100 transition-colors"
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell key={cell.id} className="whitespace-nowrap py-4 font-medium text-slate-800">
|
||||
{table.getRowModel().rows.map(row => (
|
||||
<TableRow key={row.id} className="hover:bg-slate-50/50 transition-colors">
|
||||
{row.getVisibleCells().map(cell => (
|
||||
<TableCell key={cell.id} className="p-6 font-medium text-slate-700">
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={columns.length} className="h-64 text-center">
|
||||
<div className="flex flex-col items-center justify-center gap-2 opacity-60">
|
||||
<span className="text-4xl mb-2">🎈</span>
|
||||
<p className="font-bold text-slate-500 text-lg">暂无数据落库记录</p>
|
||||
<p className="text-sm text-slate-400">目前没有任何 JSONB 数据。请到首页对 AI 输入需求填充表单。</p>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between py-4 px-6 border-t border-slate-200 bg-white shrink-0">
|
||||
<div className="text-sm font-medium text-slate-500">
|
||||
总计解析并挂载 <span className="font-bold text-indigo-600">{data.length}</span> 条动态记录
|
||||
</div>
|
||||
<div className="p-6 border-t flex justify-between items-center bg-slate-50/50">
|
||||
<span className="text-sm text-slate-500 font-bold tracking-widest">共 {data.length} 条记录</span>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => table.previousPage()}
|
||||
disabled={!table.getCanPreviousPage()}
|
||||
className="rounded-xl font-bold shadow-sm"
|
||||
<Button variant="outline" size="sm" onClick={() => table.previousPage()} disabled={!table.getCanPreviousPage()}>上页</Button>
|
||||
<Button variant="outline" size="sm" onClick={() => table.nextPage()} disabled={!table.getCanNextPage()}>下页</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 弹窗容器 */}
|
||||
{isModalOpen && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-slate-900/60 backdrop-blur-sm p-4">
|
||||
<div className="bg-white rounded-3xl shadow-2xl w-full max-w-2xl max-h-[90vh] overflow-auto relative animate-in fade-in zoom-in duration-300">
|
||||
<button
|
||||
onClick={() => setIsModalOpen(false)}
|
||||
className="absolute top-6 right-6 w-10 h-10 flex items-center justify-center rounded-full bg-slate-100 text-slate-500 hover:bg-slate-200 transition-colors z-20"
|
||||
>
|
||||
上页
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => table.nextPage()}
|
||||
disabled={!table.getCanNextPage()}
|
||||
className="rounded-xl font-bold shadow-sm"
|
||||
>
|
||||
下页
|
||||
</Button>
|
||||
</div>
|
||||
✕
|
||||
</button>
|
||||
<DynamicForm
|
||||
entityName={entity.entityName}
|
||||
entityCode={entityCode}
|
||||
jsonSchema={entity.schemaDefinition}
|
||||
initialData={editingData}
|
||||
onSuccess={() => { setIsModalOpen(false); loadData(); }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,8 @@ const geistMono = Geist_Mono({
|
||||
});
|
||||
|
||||
import Sidebar from "@/components/Sidebar";
|
||||
import CopilotProvider from "@/components/CopilotProvider";
|
||||
import AuthGuard from "@/components/AuthGuard";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "AI-Native CRM",
|
||||
@@ -30,10 +32,14 @@ export default function RootLayout({
|
||||
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
|
||||
>
|
||||
<body className="flex h-screen overflow-hidden bg-slate-50 relative">
|
||||
<AuthGuard>
|
||||
<CopilotProvider>
|
||||
<Sidebar />
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
{children}
|
||||
</div>
|
||||
</CopilotProvider>
|
||||
</AuthGuard>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
export default function LoginPage() {
|
||||
const [username, setUsername] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const router = useRouter();
|
||||
|
||||
const handleLogin = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!username.trim()) {
|
||||
alert("请输入用户名");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
// 模拟登录:直接存入 localStorage
|
||||
setTimeout(() => {
|
||||
localStorage.setItem("crm_user_id", username);
|
||||
router.push("/");
|
||||
}, 800);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen w-full flex items-center justify-center bg-[#0f172a] relative overflow-hidden">
|
||||
{/* 背景装饰 */}
|
||||
<div className="absolute top-[-10%] left-[-10%] w-[40%] h-[40%] bg-indigo-500/10 rounded-full blur-[120px] animate-pulse"></div>
|
||||
<div className="absolute bottom-[-10%] right-[-10%] w-[40%] h-[40%] bg-blue-500/10 rounded-full blur-[120px] animate-pulse delay-700"></div>
|
||||
|
||||
<div className="w-full max-w-md p-8 relative z-10">
|
||||
<div className="bg-white/5 backdrop-blur-xl border border-white/10 rounded-[2.5rem] p-10 shadow-2xl overflow-hidden group">
|
||||
<div className="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-transparent via-indigo-500 to-transparent opacity-50"></div>
|
||||
|
||||
<div className="flex flex-col items-center mb-10">
|
||||
<div className="w-16 h-16 rounded-2xl bg-gradient-to-br from-indigo-500 to-blue-600 flex items-center justify-center shadow-lg shadow-indigo-500/20 mb-6 group-hover:scale-110 transition-transform duration-500">
|
||||
<span className="text-white text-3xl font-black italic">AI</span>
|
||||
</div>
|
||||
<h1 className="text-3xl font-black text-white tracking-tight mb-2">AI-Native CRM</h1>
|
||||
<p className="text-slate-400 font-medium">智能生成架构 · 极简业务管理</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleLogin} className="space-y-6">
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-black text-slate-500 uppercase tracking-widest ml-1">用户名</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="随便输入一个用户名..."
|
||||
className="w-full bg-white/5 border border-white/10 rounded-2xl px-6 py-4 text-white outline-none focus:border-indigo-500/50 focus:bg-white/10 transition-all font-medium placeholder:text-slate-600"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-black text-slate-500 uppercase tracking-widest ml-1">密码 (选填)</label>
|
||||
<input
|
||||
type="password"
|
||||
placeholder="••••••••"
|
||||
className="w-full bg-white/5 border border-white/10 rounded-2xl px-6 py-4 text-white outline-none focus:border-indigo-500/50 focus:bg-white/10 transition-all font-medium placeholder:text-slate-600"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isLoading}
|
||||
className="w-full bg-gradient-to-r from-indigo-600 to-blue-600 hover:from-indigo-500 hover:to-blue-500 text-white font-black py-5 rounded-2xl shadow-xl shadow-indigo-500/20 transition-all active:scale-[0.98] flex items-center justify-center gap-3 disabled:opacity-50"
|
||||
>
|
||||
{isLoading ? (
|
||||
<span className="w-5 h-5 border-2 border-white/30 border-t-white rounded-full animate-spin"></span>
|
||||
) : (
|
||||
<>
|
||||
<span>进入系统</span>
|
||||
<span className="text-xl">→</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div className="mt-10 pt-8 border-t border-white/5 text-center">
|
||||
<p className="text-xs text-slate-500 font-medium leading-relaxed">
|
||||
提示: 当前为会话隔离模式,<br />
|
||||
系统将根据您的用户名独占一份业务数据。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+6
-24
@@ -1,50 +1,32 @@
|
||||
"use client";
|
||||
|
||||
import { CopilotKit } from "@copilotkit/react-core";
|
||||
import { CopilotPopup } from "@copilotkit/react-ui";
|
||||
import "@copilotkit/react-ui/styles.css";
|
||||
import UniversalModuleRenderer from "@/components/UniversalModuleRenderer";
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
// 【关键修改点】在 CopilotKit 组件上增加 provider="openai"
|
||||
<CopilotKit runtimeUrl="/api/copilotkit">
|
||||
<div className="min-h-screen bg-slate-50 font-[family-name:var(--font-geist-sans)]">
|
||||
{/* 顶部导航 */}
|
||||
<header className="bg-white shadow-sm border-b px-8 py-4 flex items-center justify-between">
|
||||
<h1 className="text-2xl font-black text-gray-800 tracking-tight">AI-Native <span className="text-blue-600">Copilot CRM</span></h1>
|
||||
<h1 className="text-2xl font-black text-gray-800 tracking-tight">AI-Native <span className="text-blue-600">CRM</span></h1>
|
||||
<div className="flex items-center space-x-4">
|
||||
<span className="text-sm font-medium text-gray-500">登录身份: <span className="text-gray-900 bg-gray-100 px-2 py-1 rounded">ADMIN_TENANT_1</span></span>
|
||||
<span className="text-sm font-medium text-gray-500">业务会话 ID: <span className="text-gray-900 bg-gray-100 px-2 py-1 rounded font-mono">{(typeof window !== 'undefined' ? localStorage.getItem("crm_user_id") : "UNKNOWN") || "UNKNOWN"}</span></span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="p-8 max-w-5xl mx-auto mt-8 flex flex-col gap-6">
|
||||
<div className="bg-white p-6 rounded-xl shadow-sm border border-gray-100 relative overflow-hidden">
|
||||
<div className="absolute top-0 right-0 w-32 h-32 bg-blue-50 rounded-bl-full -z-10 opacity-50"></div>
|
||||
<h2 className="text-xl font-bold mb-2">欢迎使用声明式工作台</h2>
|
||||
<h2 className="text-xl font-bold mb-2">👋 欢迎来到您的智能 AI 业务助理</h2>
|
||||
<h2 className="text-xl font-bold mb-2"> 测试模式,不能保存到后端! 以后发布后再测试吧</h2>
|
||||
<p className="text-gray-600 text-sm leading-relaxed max-w-2xl">
|
||||
在这个颠覆性的系统中,您不再需要联系研发排期写代码。只需点击右下角的智能体按钮,例如告诉它:<b>“我需要一个内部采购报销单,要求包含物品名称、总造价和期待到货日期”</b>,系统就会立刻为您画出表单架构。
|
||||
无需复杂的配置,直接告诉 AI 您想管理的内容。点击右下角的 <span className="text-blue-600 font-bold">Copilot 智能助手</span> 并输入您的需求,
|
||||
例如:<b>“我想创建一个员工入职登记表,包含姓名、职位和入职时间”</b>,系统将立即为您自动生成定制化的业务单据!
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="w-full bg-white border border-gray-200 border-dashed p-10 rounded-xl min-h-[400px] flex flex-col items-center justify-center relative shadow-inner">
|
||||
<svg className="w-12 h-12 text-gray-300 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 002-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"></path></svg>
|
||||
<p className="text-gray-400 font-medium">业务模块画布空空如也,等待 AI 为您注入灵魂 (Generative UI Render Area)...</p>
|
||||
</div>
|
||||
|
||||
{/* 这里挂载在页面后台,负责监听并执行 AI 下发的 UI 绘画指令 */}
|
||||
<UniversalModuleRenderer />
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<CopilotPopup
|
||||
instructions="你是一个嵌入在高级企业 CRM 系统中的智能架构师。当用户需要新建任何功能时,请调用 renderDynamicForm 工具响应用户并渲染出一个 JSON Schema 定义的表单。"
|
||||
labels={{
|
||||
title: "🤖 CRM 架构探求者",
|
||||
initial: "您好,刘总。您今天想为公司内部门添加什么新功能模块?",
|
||||
}}
|
||||
defaultOpen={true}
|
||||
/>
|
||||
</CopilotKit>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useRouter, usePathname } from "next/navigation";
|
||||
|
||||
export default function AuthGuard({ children }: { children: React.ReactNode }) {
|
||||
const [isAuthorized, setIsAuthorized] = useState(false);
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
|
||||
useEffect(() => {
|
||||
const userId = localStorage.getItem("crm_user_id");
|
||||
|
||||
if (!userId && pathname !== "/login") {
|
||||
router.push("/login");
|
||||
} else if (userId && pathname === "/login") {
|
||||
router.push("/");
|
||||
} else {
|
||||
setIsAuthorized(true);
|
||||
}
|
||||
}, [pathname, router]);
|
||||
|
||||
if (!isAuthorized && pathname !== "/login") {
|
||||
return (
|
||||
<div className="min-h-screen w-full flex items-center justify-center bg-slate-50">
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<div className="w-12 h-12 border-4 border-indigo-600/20 border-t-indigo-600 rounded-full animate-spin"></div>
|
||||
<p className="text-slate-500 font-bold animate-pulse">正在验证身份...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
"use client";
|
||||
|
||||
import { CopilotKit } from "@copilotkit/react-core";
|
||||
import { CopilotPopup } from "@copilotkit/react-ui";
|
||||
import "@copilotkit/react-ui/styles.css";
|
||||
|
||||
export default function CopilotProvider({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<CopilotKit runtimeUrl="/api/copilotkit">
|
||||
{children}
|
||||
<CopilotPopup
|
||||
instructions="你是一个嵌入在高级企业 CRM 系统中的智能架构师。你可以通过 renderDynamicForm 为用户渲染新模块。当用户询问系统中有哪些模块或需要分析现有 schema 时,你可以调用 analyzeSystemModules 获取当前系统中已定义的业务模块及其 JSON Schema 进行分析并回答用户。"
|
||||
labels={{
|
||||
title: "🤖 AI 业务助理",
|
||||
initial: "您好,汪总。您今天想为公司内部门添加什么新功能模块?",
|
||||
}}
|
||||
defaultOpen={true}
|
||||
/>
|
||||
</CopilotKit>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useMemo, useEffect } from "react";
|
||||
|
||||
/**
|
||||
* 子组件:实体关联选择器 (数据级联)
|
||||
*/
|
||||
export function EntitySelect({ targetCode, value, onChange }: { targetCode: string, value: any, onChange: (v: any) => void }) {
|
||||
const [options, setOptions] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchOptions = async () => {
|
||||
const userId = localStorage.getItem("crm_user_id") || "anonymous";
|
||||
try {
|
||||
const res = await fetch(`/api/dynamic/code/${targetCode}`, {
|
||||
headers: { 'X-Creator-Id': userId }
|
||||
});
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
setOptions(data);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("加载关联实体数据失败:", err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
fetchOptions();
|
||||
}, [targetCode]);
|
||||
|
||||
return (
|
||||
<select
|
||||
className="w-full px-6 py-5 bg-slate-50 border-2 border-transparent rounded-2xl focus:bg-white focus:border-indigo-400 outline-none text-slate-800 font-medium shadow-sm transition-all"
|
||||
value={value || ""}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
>
|
||||
<option value="">{loading ? "📦 正在拉取关联数据..." : "--- 请选择 ---"}</option>
|
||||
{options.map((opt: any) => {
|
||||
const detail = typeof opt.data === 'string' ? JSON.parse(opt.data) : opt.data;
|
||||
const label = detail.name || detail.title || detail.entityName || `记录 #${opt.id.slice(-6)}`;
|
||||
return <option key={opt.id} value={opt.id}>{label}</option>;
|
||||
})}
|
||||
</select>
|
||||
);
|
||||
}
|
||||
|
||||
interface DynamicFormProps {
|
||||
entityName: string;
|
||||
entityCode: string;
|
||||
jsonSchema: string;
|
||||
initialData?: Record<string, any>;
|
||||
onSuccess?: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用动态表单组件 (支持关联与业务逻辑)
|
||||
*/
|
||||
export default function DynamicForm({ entityName, entityCode, jsonSchema, initialData, onSuccess }: DynamicFormProps) {
|
||||
const [formData, setFormData] = useState<Record<string, any>>(initialData || {});
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
|
||||
const schemaObj = useMemo(() => {
|
||||
try {
|
||||
return JSON.parse(jsonSchema);
|
||||
} catch (e) {
|
||||
console.error("Schema 解析失败:", e);
|
||||
return null;
|
||||
}
|
||||
}, [jsonSchema]);
|
||||
|
||||
if (!schemaObj) return <div className="p-4 text-red-500 italic">正在解析中...</div>;
|
||||
|
||||
const properties = schemaObj.properties || {};
|
||||
|
||||
const handleSave = async () => {
|
||||
if (Object.keys(formData).length === 0) {
|
||||
alert("请至少填写一项数据后再保存。");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSaving(true);
|
||||
try {
|
||||
const userId = localStorage.getItem("crm_user_id") || "anonymous";
|
||||
const aiMessage = `【业务提交报文】\n` +
|
||||
`当前模块:${entityName} (${entityCode})\n` +
|
||||
`Schema 结构:${jsonSchema}\n` +
|
||||
`录入事实数据:${JSON.stringify(formData)}\n\n` +
|
||||
`请 AI 自动调度工具完成此模块的校验注册与数据入库工作。`;
|
||||
|
||||
const response = await fetch(`/api/agent/chat?sessionId=${userId}_SESSION_${entityCode}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'text/plain',
|
||||
'X-Creator-Id': userId
|
||||
},
|
||||
body: aiMessage
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const resultText = await response.text();
|
||||
alert(`✅ AI 已成功处理!\n反馈: ${resultText}`);
|
||||
window.dispatchEvent(new CustomEvent('ENTITY_CREATED'));
|
||||
if (onSuccess) onSuccess();
|
||||
setFormData({});
|
||||
} else {
|
||||
const errorMsg = await response.text();
|
||||
alert(`❌ 处理失败: ${errorMsg}`);
|
||||
}
|
||||
} catch (err) {
|
||||
alert("⚠️ 网络故障,请确保后端服务正常。");
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-8 bg-white rounded-3xl">
|
||||
<h3 className="text-2xl font-black mb-8 border-b pb-6 text-slate-800 flex items-center gap-3">
|
||||
<span className="p-2 bg-indigo-600 rounded-xl text-white">✨</span>
|
||||
录入: {entityName}
|
||||
</h3>
|
||||
|
||||
<div className="space-y-6">
|
||||
{Object.entries(properties).map(([key, field]: [string, any]) => {
|
||||
const targetEntity = field['x-link-entity'];
|
||||
const enumOptions = field.enum;
|
||||
|
||||
const isRequired = schemaObj.required?.includes(key) || field.required === true;
|
||||
|
||||
return (
|
||||
<div key={key} className="flex flex-col space-y-2">
|
||||
<label className="text-sm font-bold text-slate-500 uppercase flex items-center gap-2">
|
||||
{field.title || key}
|
||||
{isRequired && (
|
||||
<span className="inline-flex items-center justify-center bg-gradient-to-r from-red-500 via-pink-500 to-rose-600 bg-clip-text text-transparent font-black text-xl leading-none select-none animate-pulse">
|
||||
*
|
||||
</span>
|
||||
)}
|
||||
</label>
|
||||
|
||||
{targetEntity ? (
|
||||
<EntitySelect
|
||||
targetCode={targetEntity}
|
||||
value={formData[key]}
|
||||
onChange={(v) => setFormData(prev => ({ ...prev, [key]: v }))}
|
||||
/>
|
||||
) : enumOptions ? (
|
||||
<select
|
||||
className="w-full px-6 py-4 bg-slate-50 border-2 border-transparent rounded-2xl focus:bg-white focus:border-indigo-400 outline-none text-slate-800 font-medium"
|
||||
value={formData[key] || ""}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, [key]: e.target.value }))}
|
||||
>
|
||||
<option value="">请选择...</option>
|
||||
{enumOptions.map((opt: any) => <option key={opt} value={opt}>{opt}</option>)}
|
||||
</select>
|
||||
) : (
|
||||
<input
|
||||
className="w-full px-6 py-4 bg-slate-50 border-2 border-transparent rounded-2xl focus:bg-white focus:border-indigo-400 outline-none text-slate-800 font-medium"
|
||||
type={field.format === 'date' ? 'date' : (field.type === 'number' ? 'number' : 'text')}
|
||||
placeholder={`请输入 ${field.title || key}...`}
|
||||
value={formData[key] || ""}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, [key]: e.target.value }))}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={isSaving}
|
||||
className="w-full mt-10 p-5 bg-gradient-to-br from-indigo-600 to-blue-700 text-white rounded-2xl font-black text-lg shadow-xl"
|
||||
>
|
||||
{isSaving ? "AI 处理中..." : "🚀 提交业务记录"}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -3,15 +3,27 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useCopilotReadable } from "@copilotkit/react-core";
|
||||
|
||||
export default function Sidebar() {
|
||||
const [entities, setEntities] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const pathname = usePathname();
|
||||
|
||||
useEffect(() => {
|
||||
// 暴露当前系统中的模块列表给 Copilot 智能体,使其能够回答“系统里有什么模块”这类问题
|
||||
useCopilotReadable({
|
||||
description: "当前系统中已定义的业务模块列表(包括 entityCode, entityName, schemaDefinition)",
|
||||
value: entities,
|
||||
});
|
||||
|
||||
const fetchEntities = () => {
|
||||
const userId = localStorage.getItem("crm_user_id") || "anonymous";
|
||||
// Asynchronously fetch from API
|
||||
fetch("http://localhost:8080/api/entities")
|
||||
fetch("/api/entities", {
|
||||
headers: {
|
||||
'X-Creator-Id': userId
|
||||
}
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
setEntities(data);
|
||||
@@ -21,6 +33,21 @@ export default function Sidebar() {
|
||||
console.error("Failed to fetch entities", err);
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchEntities();
|
||||
|
||||
// 1. 监听自定义事件触发即时刷新 (AI 生成或手动提交后)
|
||||
window.addEventListener('ENTITY_CREATED', fetchEntities);
|
||||
|
||||
// 2. 兜底方案:每 10 秒主动拉取一次,确保其他 Session 的变动可见
|
||||
const timer = setInterval(fetchEntities, 30000);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('ENTITY_CREATED', fetchEntities);
|
||||
clearInterval(timer);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
@@ -30,7 +57,7 @@ export default function Sidebar() {
|
||||
<span className="text-white text-xl font-black">AI</span>
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-xl font-black tracking-tight text-white">Copilot CRM</h2>
|
||||
<h2 className="text-xl font-black tracking-tight text-white">AI-Native CRM</h2>
|
||||
<p className="text-xs text-slate-500 font-medium">智能生成架构</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -47,7 +74,7 @@ export default function Sidebar() {
|
||||
|
||||
{!loading && entities.length === 0 ? (
|
||||
<div className="px-4 py-8 text-sm text-slate-500 text-center bg-slate-800/20 rounded-2xl border border-slate-800/50 border-dashed">
|
||||
尚未生成任何模块<br/><span className="text-xs mt-2 block">试着跟右下角 AI 说说话</span>
|
||||
尚未生成任何模块<br /><span className="text-xs mt-2 block">试着跟右下角 AI 说说话</span>
|
||||
</div>
|
||||
) : (
|
||||
entities.map((ent, idx) => {
|
||||
@@ -70,14 +97,23 @@ export default function Sidebar() {
|
||||
<div className="p-6 border-t border-slate-800/50 relative overflow-hidden">
|
||||
<div className="absolute top-0 right-0 w-32 h-32 bg-indigo-500/10 rounded-bl-full -z-10 blur-xl"></div>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-full bg-slate-800 border-2 border-slate-700 flex items-center justify-center font-bold text-white relative">
|
||||
AD
|
||||
<div className="w-10 h-10 rounded-full bg-slate-800 border-2 border-slate-700 flex items-center justify-center font-bold text-white relative uppercase">
|
||||
{(typeof window !== 'undefined' ? localStorage.getItem("crm_user_id") : "AD")?.slice(0, 2) || "AD"}
|
||||
<div className="absolute bottom-0 right-0 w-3 h-3 bg-green-400 rounded-full border-2 border-slate-900"></div>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-bold text-white uppercase tracking-wider">ADMIN 1</p>
|
||||
<p className="text-xs text-slate-500">超级管理员租户</p>
|
||||
<p className="text-sm font-bold text-white uppercase tracking-wider truncate max-w-[120px]">
|
||||
{typeof window !== 'undefined' ? localStorage.getItem("crm_user_id") : "ADMIN 1"}
|
||||
</p>
|
||||
<p className="text-xs text-slate-500">当前业务会话</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => { localStorage.removeItem("crm_user_id"); window.location.reload(); }}
|
||||
className="ml-auto text-slate-600 hover:text-rose-400 p-2 transition-colors"
|
||||
title="退出登录"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
@@ -7,132 +7,71 @@ import { useState, useMemo, useEffect } from "react";
|
||||
* 子组件:动态表单渲染实体 (State 隔离,解决输入失效问题)
|
||||
* 该组件拥有独立的生命周期和 State,确保在输入时由于父组件重绘不会导致失去焦点。
|
||||
*/
|
||||
function DynamicForm({ args }: { args: any }) {
|
||||
// 状态管理:用于收集动态表单中用户填入的各字段值
|
||||
const [formData, setFormData] = useState<Record<string, any>>({});
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
|
||||
// 1. 防御性检查:确保输入参数有效
|
||||
if (!args || !args.jsonSchema) return null;
|
||||
|
||||
// 2. 解析 JSON Schema (使用 useMemo 避免每次渲染都重解析)
|
||||
const schemaObj = useMemo(() => {
|
||||
try {
|
||||
return JSON.parse(args.jsonSchema);
|
||||
} catch (e) {
|
||||
console.error("Schema 解析失败:", e);
|
||||
return null;
|
||||
}
|
||||
}, [args.jsonSchema]);
|
||||
|
||||
if (!schemaObj) return <div className="p-4 text-red-500 italic">正在解析中...</div>;
|
||||
|
||||
const properties = schemaObj.properties || {};
|
||||
|
||||
// 落地数据库逻辑
|
||||
const handleSave = async () => {
|
||||
if (Object.keys(formData).length === 0) {
|
||||
alert("请至少填写一项数据后再保存。");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSaving(true);
|
||||
try {
|
||||
// 优先确保后端已经创建了该模块定义(解决时序问题:AI可能在前端直接渲染,而后端还未来得及/或者没有注册模块)
|
||||
try {
|
||||
await fetch(`http://localhost:8080/api/dynamic/schema`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
// 注意:后端接受属性包括 entityCode, entityName, schemaDefinition
|
||||
body: JSON.stringify({
|
||||
entityCode: args.entityCode,
|
||||
entityName: args.entityName,
|
||||
schemaDefinition: args.jsonSchema
|
||||
})
|
||||
});
|
||||
} catch (err) {
|
||||
console.warn("提前注册模块时发生网络异常:", err);
|
||||
}
|
||||
|
||||
const response = await fetch(`http://localhost:8080/api/dynamic/code/${args.entityCode}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Creator-Id': 'ADMIN_001'
|
||||
},
|
||||
body: JSON.stringify(formData)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
alert(`✅ 数据落库成功!已存入 PostgreSQL。\n模块: ${args.entityName}`);
|
||||
setFormData({}); // 清空表单
|
||||
} else {
|
||||
const errorMsg = await response.text();
|
||||
alert(`❌ 虽然表单生成了,但保存失败: ${errorMsg}`);
|
||||
}
|
||||
} catch (err) {
|
||||
alert("⚠️ 网络故障,请确保后端 8080 服务正在运行。");
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-10 border-2 border-indigo-100 bg-white rounded-3xl shadow-2xl mt-8">
|
||||
<h3 className="text-3xl font-black mb-8 border-b pb-6 text-slate-800 flex items-center gap-3">
|
||||
<span className="p-2 bg-indigo-600 rounded-xl text-white">✨</span>
|
||||
{args.entityName}
|
||||
</h3>
|
||||
|
||||
<div className="space-y-8">
|
||||
{Object.entries(properties).map(([key, field]: [string, any]) => (
|
||||
<div key={key} className="flex flex-col space-y-3">
|
||||
<label className="text-sm font-bold text-slate-500 uppercase flex items-center gap-2">
|
||||
{field.title || key}
|
||||
{field.type === 'number' && <span className="text-[10px] bg-slate-100 text-slate-400 px-1.5 py-0.5 rounded">数字</span>}
|
||||
</label>
|
||||
<input
|
||||
className="w-full px-6 py-5 bg-slate-50 border-2 border-transparent rounded-2xl focus:bg-white focus:border-indigo-400 focus:ring-8 focus:ring-indigo-50 transition-all outline-none text-slate-800 font-medium shadow-sm"
|
||||
placeholder={`请输入 ${field.title || key}...`}
|
||||
value={formData[key] || ""}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, [key]: e.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={isSaving}
|
||||
className="w-full mt-12 p-6 bg-gradient-to-br from-indigo-600 to-blue-700 text-white rounded-2xl font-black text-xl hover:shadow-2xl hover:translate-y-[-2px] active:translate-y-[1px] transition-all disabled:opacity-50"
|
||||
>
|
||||
{isSaving ? "正在同步到数据库..." : "🚀 确认提交所有录入数据"}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
import DynamicForm from "./DynamicForm";
|
||||
|
||||
/**
|
||||
* 主入口:通用模块渲染器
|
||||
* 负责注册 AI Action,并将参数转发给解耦后的 DynamicForm 组件。
|
||||
*/
|
||||
export default function UniversalModuleRenderer() {
|
||||
const [activeForm, setActiveForm] = useState<{ entityName: string, entityCode: string, jsonSchema: string } | null>(null);
|
||||
|
||||
useCopilotAction({
|
||||
name: "renderDynamicForm",
|
||||
description: "在页面上为一个新的业务模块渲染基于 JSON Schema 的动态表单。",
|
||||
description: "在页面上方展示并渲染基于 JSON Schema 的动态表单。",
|
||||
parameters: [
|
||||
{ name: "entityName", type: "string" },
|
||||
{ name: "entityCode", type: "string" },
|
||||
{ name: "jsonSchema", type: "string" }
|
||||
{ name: "entityName", type: "string", description: "模块名称" },
|
||||
{ name: "entityCode", type: "string", description: "模块编码" },
|
||||
{ name: "jsonSchema", type: "string", description: "JSON Schema 定义" }
|
||||
],
|
||||
handler: async (args) => {
|
||||
// 通过 AI 下发指令时,设置状态让页面渲染
|
||||
setActiveForm({
|
||||
entityName: args.entityName,
|
||||
entityCode: args.entityCode,
|
||||
jsonSchema: args.jsonSchema,
|
||||
});
|
||||
window.dispatchEvent(new CustomEvent('ENTITY_CREATED'));
|
||||
return `已确认 ${args.entityName} 的表单渲染指令。`;
|
||||
},
|
||||
render: (props: any) => {
|
||||
// 将 AI 的渲染逻辑委托给 DynamicForm,实现 State 永久隔离,彻底解决输入焦点问题。
|
||||
return <DynamicForm args={props.args} />;
|
||||
});
|
||||
|
||||
// 动作 2:主动拉取并分析系统中已有的模块
|
||||
useCopilotAction({
|
||||
name: "analyzeSystemModules",
|
||||
description: "从后端接口获取当前系统中已有的所有业务模块及其 JSON Schema 定义进行分析。",
|
||||
handler: async () => {
|
||||
try {
|
||||
const res = await fetch("/api/entities");
|
||||
const data = await res.json();
|
||||
if (data.length === 0) {
|
||||
return "系统中当前没有任何业务模块。";
|
||||
}
|
||||
return `系统中目前有 ${data.length} 个模块:\n` +
|
||||
data.map((m: any) => `- ${m.entityName} (${m.entityCode}): ${m.schemaDefinition}`).join("\n");
|
||||
} catch (err) {
|
||||
return "从后端获取模块列表失败。";
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return null;
|
||||
if (!activeForm) {
|
||||
return (
|
||||
<div className="w-full bg-white border border-gray-200 border-dashed p-10 rounded-xl min-h-[400px] flex flex-col items-center justify-center relative shadow-inner">
|
||||
<svg className="w-12 h-12 text-gray-300 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 002-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"></path></svg>
|
||||
<p className="text-gray-400 font-medium italic">业务模块画布空空如也,等待 AI 为您注入灵魂...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mt-8 border-2 border-indigo-100 rounded-3xl shadow-2xl overflow-hidden bg-white animate-in fade-in slide-in-from-bottom-5 duration-700">
|
||||
<DynamicForm
|
||||
entityName={activeForm.entityName}
|
||||
entityCode={activeForm.entityCode}
|
||||
jsonSchema={activeForm.jsonSchema}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user