Browse Source

feat: 重构主布局并优化动态表单功能

重构主布局组件,将侧边栏和 Copilot 提供者逻辑提取到 MainLayout 组件
优化动态表单,支持编辑模式并完善错误处理
移除测试提示信息并更新环境变量配置
main
Boom 1 week ago
parent
commit
0d3d09f280
  1. 16
      .antigravity/BASELINE.md
  2. 32
      src/app/api/copilotkit/route.ts
  3. 21
      src/app/data/[entityCode]/page.tsx
  4. 10
      src/app/layout.tsx
  5. 1
      src/app/page.tsx
  6. 2
      src/components/CopilotProvider.tsx
  7. 57
      src/components/DynamicForm.tsx
  8. 26
      src/components/MainLayout.tsx

16
.antigravity/BASELINE.md

@ -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*

32
src/app/api/copilotkit/route.ts

@ -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);
};

21
src/app/data/[entityCode]/page.tsx

@ -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 已同步执行删除操作。");
loadData();
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);
};

10
src/app/layout.tsx

@ -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">
{children}
</div>
</CopilotProvider>
<MainLayout>
{children}
</MainLayout>
</AuthGuard>
</body>
</html>

1
src/app/page.tsx

@ -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>

2
src/components/CopilotProvider.tsx

@ -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 进行分析并回答用户。"

57
src/components/DynamicForm.tsx

@ -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` +
`当前模块:${entityName} (${entityCode})\n` +
`Schema 结构:${jsonSchema}\n` +
`录入事实数据:${JSON.stringify(formData)}\n\n` +
`请 AI 自动调度工具完成此模块的校验注册与数据入库工作。`;
const aiMessage = isEdit
? `【业务更新报文】\n` +
`操作类型:更新已有记录\n` +
`记录 ID:${recordId}\n` +
`当前模块:${entityName} (${entityCode})\n` +
`更新后数据:${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}`);
window.dispatchEvent(new CustomEvent('ENTITY_CREATED'));
if (onSuccess) onSuccess();
setFormData({});
const result = await response.json();
if (result.code === 200) {
window.dispatchEvent(new CustomEvent('ENTITY_CREATED'));
if (onSuccess) onSuccess();
setFormData({});
} else {
alert(`操作失败: ${result.msg || "未知错误"}`);
}
} else {
const errorMsg = await response.text();
alert(`❌ 处理失败: ${errorMsg}`);
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>
);

26
src/components/MainLayout.tsx

@ -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>
);
}
Loading…
Cancel
Save