feat: 重构主布局并优化动态表单功能
重构主布局组件,将侧边栏和 Copilot 提供者逻辑提取到 MainLayout 组件 优化动态表单,支持编辑模式并完善错误处理 移除测试提示信息并更新环境变量配置
This commit is contained in:
@@ -5,8 +5,9 @@
|
||||
- **核心理念**:通过 AI 驱动的消息流实现动态 UI 生成和业务处理。
|
||||
- **技术栈**:Next.js 15 (App Router) + Tailwind CSS + Lucide Icons。
|
||||
- **关键组件**:
|
||||
- `UniversalModuleRenderer.tsx`: 后端 AI 下发 UI 指令的监听与执行枢纽,负责实时绘制界面。
|
||||
- `DynamicForm.tsx`: 基于 JSON Schema 的动态表单渲染器,支持实体关联选择 (`x-link-entity`)。
|
||||
- `UniversalModuleRenderer.tsx`: 后端 AI 下发 UI 指令的监听与执行枢纽。
|
||||
- `CopilotProvider.tsx`: **已更新**,取消本地 AI 运行时,改为直连后端 `/api/copilotkit` 端口。
|
||||
- `DynamicForm.tsx`: 基于 JSON Schema 的动态表单渲染器。
|
||||
- `EntityDataPage`: 动态列表展示页面,通过 AI 代理执行编辑和删除操作。
|
||||
|
||||
## 💼 业务逻辑 (Business Logic)
|
||||
@@ -16,17 +17,16 @@
|
||||
## 📈 当前开发进度 (Current Progress)
|
||||
- [x] **AI 动态 UI 渲染**:`UniversalModuleRenderer` 基础架构就绪。
|
||||
- [x] **动态表单与关联**:支持枚举、模式匹配及实体级联选择。
|
||||
- [x] **会话隔离适配**:前端已实现 Header 注入逻辑。
|
||||
- [x] **CORS 联调**:已完成与后端的跨域联调。
|
||||
- [x] **安全优化**:移除了前端 API Key,所有 AI 请求通过后端转发。
|
||||
- [x] **业务模块共享**:适配后端变更,侧边栏现可按公共定义渲染所有活跃模块。
|
||||
|
||||
## ⚠️ 尚未解决的隐患 (Known Issues/Risks)
|
||||
- **共享冲突提示**:缺乏对公共模块并发编辑的 UI 级冲突提示。
|
||||
- **状态同步**:多组件间由于 AI 异步返回导致的 UI 闪烁问题。
|
||||
- **离线能力**:目前完全依赖实时 AI 交互,缺乏离线暂存机制。
|
||||
|
||||
## 🚀 下一步计划 (Next Steps)
|
||||
1. **AI 智能搜索界面**:对接后端的 AI 增强搜索功能。
|
||||
2. **动态看板 (Dashboard)**:实现可视化图表的 AI 渲染支持。
|
||||
3. **引导流程优化**:提升用户通过对话创建新模块的体验。
|
||||
1. **数据权限展示**:在 UI 层面区分公共数据与私人数据。
|
||||
2. **AI 智能搜索界面**:对接后端的 AI 增强搜索功能。
|
||||
|
||||
---
|
||||
*Last Updated: 2026-03-25 by Antigravity*
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
import {
|
||||
CopilotRuntime,
|
||||
GoogleGenerativeAIAdapter,
|
||||
copilotRuntimeNextJSAppRouterEndpoint
|
||||
} from "@copilotkit/runtime";
|
||||
import { GoogleGenerativeAI } from "@google/generative-ai";
|
||||
|
||||
export const POST = async (req: Request) => {
|
||||
// 1. 初始化 Google Gemini 客户端
|
||||
const genAI = new GoogleGenerativeAI(process.env.GOOGLE_API_KEY || "");
|
||||
|
||||
// 2. 选择模型 (使用 3.1 Pro)
|
||||
const model = genAI.getGenerativeModel({
|
||||
model: "gemini-3.1-pro",
|
||||
// 如果需要启用思维模型等高级参数,可以在这里或 adapter 层级配置
|
||||
});
|
||||
|
||||
const runtime = new CopilotRuntime();
|
||||
|
||||
// 3. 使用 GoogleGenerativeAIAdapter
|
||||
const serviceAdapter = new GoogleGenerativeAIAdapter({
|
||||
model: "gemini-1.5-pro"
|
||||
});
|
||||
|
||||
const { handleRequest } = copilotRuntimeNextJSAppRouterEndpoint({
|
||||
runtime,
|
||||
serviceAdapter,
|
||||
endpoint: "/api/copilotkit",
|
||||
});
|
||||
|
||||
return handleRequest(req);
|
||||
};
|
||||
@@ -123,27 +123,32 @@ export default function EntityDataPage({ params: paramsPromise }: DataPageProps)
|
||||
}, [entityCode]);
|
||||
|
||||
const handleDelete = async (id: string) => {
|
||||
if (!confirm("确定要物理删除这条数据吗?该操作不可撤销,将由 AI 执行。")) return;
|
||||
if (!confirm("确定要删除这条数据吗?该操作不可撤销。")) 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} 的数据。`
|
||||
body: `请执行删除操作:物理删除记录 ID 为 ${id} 的数据。`
|
||||
});
|
||||
if (res.ok) {
|
||||
alert("✅ AI 已同步执行删除操作。");
|
||||
const result = await res.json();
|
||||
if (result.code === 200) {
|
||||
loadData();
|
||||
} else {
|
||||
alert(`删除失败: ${result.msg || "未知错误"}`);
|
||||
}
|
||||
} else {
|
||||
alert("删除失败: 请求异常");
|
||||
}
|
||||
} catch (err) {
|
||||
alert("删除失败");
|
||||
alert("删除失败: 网络故障");
|
||||
}
|
||||
};
|
||||
|
||||
const handleEdit = (row: any) => {
|
||||
// 排除系统字段
|
||||
const { _id, _createdAt, ...businessData } = row;
|
||||
setEditingData(businessData);
|
||||
const { _createdAt, ...editData } = row;
|
||||
setEditingData(editData);
|
||||
setIsModalOpen(true);
|
||||
};
|
||||
|
||||
|
||||
+3
-5
@@ -15,6 +15,7 @@ const geistMono = Geist_Mono({
|
||||
import Sidebar from "@/components/Sidebar";
|
||||
import CopilotProvider from "@/components/CopilotProvider";
|
||||
import AuthGuard from "@/components/AuthGuard";
|
||||
import MainLayout from "@/components/MainLayout";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "AI-Native CRM",
|
||||
@@ -33,12 +34,9 @@ export default function RootLayout({
|
||||
>
|
||||
<body className="flex h-screen overflow-hidden bg-slate-50 relative">
|
||||
<AuthGuard>
|
||||
<CopilotProvider>
|
||||
<Sidebar />
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
<MainLayout>
|
||||
{children}
|
||||
</div>
|
||||
</CopilotProvider>
|
||||
</MainLayout>
|
||||
</AuthGuard>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -17,7 +17,6 @@ export default function Home() {
|
||||
<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">👋 欢迎来到您的智能 AI 业务助理</h2>
|
||||
<h2 className="text-xl font-bold mb-2"> 测试模式,不能保存到后端! 以后发布后再测试吧</h2>
|
||||
<p className="text-gray-600 text-sm leading-relaxed max-w-2xl">
|
||||
无需复杂的配置,直接告诉 AI 您想管理的内容。点击右下角的 <span className="text-blue-600 font-bold">Copilot 智能助手</span> 并输入您的需求,
|
||||
例如:<b>“我想创建一个员工入职登记表,包含姓名、职位和入职时间”</b>,系统将立即为您自动生成定制化的业务单据!
|
||||
|
||||
@@ -6,7 +6,7 @@ import "@copilotkit/react-ui/styles.css";
|
||||
|
||||
export default function CopilotProvider({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<CopilotKit runtimeUrl="/api/copilotkit">
|
||||
<CopilotKit runtimeUrl={(process.env.NEXT_PUBLIC_API_URL || "http://localhost:8080") + "/api/copilotkit"}>
|
||||
{children}
|
||||
<CopilotPopup
|
||||
instructions="你是一个嵌入在高级企业 CRM 系统中的智能架构师。你可以通过 renderDynamicForm 为用户渲染新模块。当用户询问系统中有哪些模块或需要分析现有 schema 时,你可以调用 analyzeSystemModules 获取当前系统中已定义的业务模块及其 JSON Schema 进行分析并回答用户。"
|
||||
|
||||
@@ -53,11 +53,17 @@ interface DynamicFormProps {
|
||||
onSuccess?: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用动态表单组件 (支持关联与业务逻辑)
|
||||
*/
|
||||
export default function DynamicForm({ entityName, entityCode, jsonSchema, initialData, onSuccess }: DynamicFormProps) {
|
||||
const [formData, setFormData] = useState<Record<string, any>>(initialData || {});
|
||||
const recordId = initialData?._id;
|
||||
const isEdit = !!recordId;
|
||||
|
||||
const buildInitial = () => {
|
||||
if (!initialData) return {};
|
||||
const { _id, ...rest } = initialData;
|
||||
return rest;
|
||||
};
|
||||
|
||||
const [formData, setFormData] = useState<Record<string, any>>(buildInitial);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
|
||||
const schemaObj = useMemo(() => {
|
||||
@@ -82,11 +88,19 @@ export default function DynamicForm({ entityName, entityCode, jsonSchema, initia
|
||||
setIsSaving(true);
|
||||
try {
|
||||
const userId = localStorage.getItem("crm_user_id") || "anonymous";
|
||||
const aiMessage = `【业务提交报文】\n` +
|
||||
|
||||
const aiMessage = isEdit
|
||||
? `【业务更新报文】\n` +
|
||||
`操作类型:更新已有记录\n` +
|
||||
`记录 ID:${recordId}\n` +
|
||||
`当前模块:${entityName} (${entityCode})\n` +
|
||||
`Schema 结构:${jsonSchema}\n` +
|
||||
`录入事实数据:${JSON.stringify(formData)}\n\n` +
|
||||
`请 AI 自动调度工具完成此模块的校验注册与数据入库工作。`;
|
||||
`更新后数据:${JSON.stringify(formData)}\n\n` +
|
||||
`请调用 updateDynamicData 工具,使用上述 ID 和数据完成更新。`
|
||||
: `【业务提交报文】\n` +
|
||||
`操作类型:新增记录\n` +
|
||||
`当前模块:${entityName} (${entityCode})\n` +
|
||||
`录入数据:${JSON.stringify(formData)}\n\n` +
|
||||
`请调用 insertDynamicData 工具完成数据入库。`;
|
||||
|
||||
const response = await fetch(`/api/agent/chat?sessionId=${userId}_SESSION_${entityCode}`, {
|
||||
method: 'POST',
|
||||
@@ -98,17 +112,20 @@ export default function DynamicForm({ entityName, entityCode, jsonSchema, initia
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const resultText = await response.text();
|
||||
alert(`✅ AI 已成功处理!\n反馈: ${resultText}`);
|
||||
const result = await response.json();
|
||||
if (result.code === 200) {
|
||||
window.dispatchEvent(new CustomEvent('ENTITY_CREATED'));
|
||||
if (onSuccess) onSuccess();
|
||||
setFormData({});
|
||||
} else {
|
||||
const errorMsg = await response.text();
|
||||
alert(`❌ 处理失败: ${errorMsg}`);
|
||||
alert(`操作失败: ${result.msg || "未知错误"}`);
|
||||
}
|
||||
} else {
|
||||
const error = await response.json().catch(() => ({ msg: "请求失败" }));
|
||||
alert(`操作失败: ${error.msg || "未知错误"}`);
|
||||
}
|
||||
} catch (err) {
|
||||
alert("⚠️ 网络故障,请确保后端服务正常。");
|
||||
alert("网络故障,请确保后端服务正常。");
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
@@ -117,8 +134,8 @@ export default function DynamicForm({ entityName, entityCode, jsonSchema, initia
|
||||
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}
|
||||
<span className="p-2 bg-indigo-600 rounded-xl text-white">{isEdit ? "✏️" : "✨"}</span>
|
||||
{isEdit ? "编辑" : "录入"}: {entityName}
|
||||
</h3>
|
||||
|
||||
<div className="space-y-6">
|
||||
@@ -173,7 +190,7 @@ export default function DynamicForm({ entityName, entityCode, jsonSchema, initia
|
||||
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 处理中..." : "🚀 提交业务记录"}
|
||||
{isSaving ? "AI 处理中..." : (isEdit ? "💾 保存修改" : "🚀 提交业务记录")}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
"use client";
|
||||
|
||||
import { usePathname } from "next/navigation";
|
||||
import Sidebar from "./Sidebar";
|
||||
import CopilotProvider from "./CopilotProvider";
|
||||
|
||||
/**
|
||||
* 主布局组件,根据当前路径决定是否渲染侧边栏和 AI 助理
|
||||
*/
|
||||
export default function MainLayout({ children }: { children: React.ReactNode }) {
|
||||
const pathname = usePathname();
|
||||
const isLoginPage = pathname === "/login";
|
||||
|
||||
if (isLoginPage) {
|
||||
return <main className="w-full h-full">{children}</main>;
|
||||
}
|
||||
|
||||
return (
|
||||
<CopilotProvider>
|
||||
<Sidebar />
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
{children}
|
||||
</div>
|
||||
</CopilotProvider>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user