|
|
|
|
@ -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,78 +37,116 @@ export default function EntityDataPage({ params: paramsPromise }: DataPageProps)
|
|
|
|
|
const [sorting, setSorting] = useState<SortingState>([]); |
|
|
|
|
const [columns, setColumns] = useState<ColumnDef<any>[]>([]); |
|
|
|
|
|
|
|
|
|
useEffect(() => { |
|
|
|
|
async function loadData() { |
|
|
|
|
setLoading(true); |
|
|
|
|
try { |
|
|
|
|
const [entityRes, dataRes] = await Promise.all([ |
|
|
|
|
fetch(`http://localhost:8080/api/entities/code/${entityCode}`), |
|
|
|
|
fetch(`http://localhost:8080/api/dynamic/code/${entityCode}`) |
|
|
|
|
]); |
|
|
|
|
|
|
|
|
|
if (entityRes.ok && dataRes.ok) { |
|
|
|
|
const entityData = await entityRes.json(); |
|
|
|
|
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 { |
|
|
|
|
_id: r.id,
|
|
|
|
|
_createdAt: r.createdAt,
|
|
|
|
|
...parsedData
|
|
|
|
|
}; |
|
|
|
|
}); |
|
|
|
|
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) => { |
|
|
|
|
const prop = schema.properties[key]; |
|
|
|
|
dynamicColumns.push({ |
|
|
|
|
accessorKey: key, |
|
|
|
|
header: prop.title || key, |
|
|
|
|
cell: ({ row }) => { |
|
|
|
|
const cellValue = row.getValue(key); |
|
|
|
|
return cellValue !== undefined && cellValue !== null ? String(cellValue) : "-"; |
|
|
|
|
} |
|
|
|
|
}); |
|
|
|
|
}); |
|
|
|
|
} |
|
|
|
|
} catch(e) {
|
|
|
|
|
console.error("Schema Parsing Error:", e); |
|
|
|
|
} |
|
|
|
|
// 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(`/api/entities/code/${entityCode}`, { headers: { 'X-Creator-Id': userId } }), |
|
|
|
|
fetch(`/api/dynamic/code/${entityCode}`, { headers: { 'X-Creator-Id': userId } }) |
|
|
|
|
]); |
|
|
|
|
|
|
|
|
|
if (entityRes.ok && dataRes.ok) { |
|
|
|
|
const entityData = await entityRes.json(); |
|
|
|
|
const records = await dataRes.json(); |
|
|
|
|
setEntity(entityData); |
|
|
|
|
|
|
|
|
|
const mappedData = records.map((r: any) => { |
|
|
|
|
const parsedData = (typeof r.data === 'string') ? JSON.parse(r.data) : r.data; |
|
|
|
|
return { |
|
|
|
|
_id: r.id, |
|
|
|
|
_createdAt: r.createdAt, |
|
|
|
|
...parsedData |
|
|
|
|
}; |
|
|
|
|
}); |
|
|
|
|
setData(mappedData); |
|
|
|
|
|
|
|
|
|
const dynamicColumns: ColumnDef<any>[] = []; |
|
|
|
|
if (entityData && entityData.schemaDefinition) { |
|
|
|
|
const schema = JSON.parse(entityData.schemaDefinition); |
|
|
|
|
if (schema.properties) { |
|
|
|
|
Object.keys(schema.properties).forEach((key) => { |
|
|
|
|
const prop = schema.properties[key]; |
|
|
|
|
dynamicColumns.push({ |
|
|
|
|
accessorKey: key, |
|
|
|
|
header: prop.title || key, |
|
|
|
|
cell: ({ row }) => { |
|
|
|
|
const val = row.getValue(key); |
|
|
|
|
return val !== undefined && val !== null ? String(val) : "-"; |
|
|
|
|
} |
|
|
|
|
}); |
|
|
|
|
}); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// System column created at
|
|
|
|
|
dynamicColumns.push({ |
|
|
|
|
accessorKey: "_createdAt", |
|
|
|
|
header: "创建时间", |
|
|
|
|
cell: ({ row }) => { |
|
|
|
|
const val: string = row.getValue("_createdAt"); |
|
|
|
|
return val ? new Date(val).toLocaleString() : "-"; |
|
|
|
|
} |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
setColumns(dynamicColumns); |
|
|
|
|
} else { |
|
|
|
|
console.error("Failed to fetch entity or data"); |
|
|
|
|
} |
|
|
|
|
} catch (err) { |
|
|
|
|
console.error("Error loading table data:", err); |
|
|
|
|
} finally { |
|
|
|
|
setLoading(false); |
|
|
|
|
|
|
|
|
|
// 添加操作列
|
|
|
|
|
dynamicColumns.push({ |
|
|
|
|
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); |
|
|
|
|
} |
|
|
|
|
} catch (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() |
|
|
|
|
)} |
|
|
|
|
</TableHead> |
|
|
|
|
<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"> |
|
|
|
|
{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> |
|
|
|
|
{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> |
|
|
|
|
)} |
|
|
|
|
))} |
|
|
|
|
</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> |
|
|
|
|
<Button |
|
|
|
|
variant="outline" |
|
|
|
|
size="sm" |
|
|
|
|
onClick={() => table.nextPage()} |
|
|
|
|
disabled={!table.getCanNextPage()} |
|
|
|
|
className="rounded-xl font-bold shadow-sm" |
|
|
|
|
> |
|
|
|
|
下页 |
|
|
|
|
</Button> |
|
|
|
|
<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> |
|
|
|
|
<DynamicForm |
|
|
|
|
entityName={entity.entityName} |
|
|
|
|
entityCode={entityCode} |
|
|
|
|
jsonSchema={entity.schemaDefinition} |
|
|
|
|
initialData={editingData} |
|
|
|
|
onSuccess={() => { setIsModalOpen(false); loadData(); }} |
|
|
|
|
/> |
|
|
|
|
</div> |
|
|
|
|
</div> |
|
|
|
|
)} |
|
|
|
|
</div> |
|
|
|
|
); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|