Files
leaudit-platform-frontend/app/routes/prompts._index.tsx
T
LiangShiyong 33f10896a0 fix: 1.接入ai_suggestion.
2. 接入合同起草功能。
2025-12-05 00:04:45 +08:00

556 lines
17 KiB
TypeScript

import { MetaFunction, type LoaderFunctionArgs, type ActionFunctionArgs } from "@remix-run/node";
import { useSearchParams, useNavigate, useLoaderData, useFetcher, useRouteLoaderData } from "@remix-run/react";
import { useState, useEffect } from "react";
import indexStyles from "~/styles/pages/prompts_index.css?url";
import { Card } from "~/components/ui/Card";
import { Button } from "~/components/ui/Button";
import { FilterPanel, FilterSelect, SearchFilter } from "~/components/ui/FilterPanel";
import { Table } from "~/components/ui/Table";
import { Pagination } from "~/components/ui/Pagination";
import { getPromptTemplates, deletePromptTemplate, type PromptTemplateUI } from "~/api/prompts/prompts";
import { toastService, messageService } from "~/components/ui";
import { usePermission, PermissionGuard } from "~/hooks/usePermission";
// 样式链接
export function links() {
return [{ rel: "stylesheet", href: indexStyles }];
}
// 页面元数据
export const meta: MetaFunction = () => {
return [
{ title: "提示词模板管理 - 中国烟草AI合同及卷宗审核系统" },
{ name: "description", content: "管理提示词模板,包括创建、编辑和删除提示词模板" },
];
};
// 定义加载器返回数据类型
interface LoaderData {
templates: PromptTemplateUI[];
total: number;
pageSize: number;
currentPage: number;
error?: string;
}
// 定义 Action 返回数据类型
interface ActionData {
success: boolean;
error?: string;
}
// 数据加载器
export async function loader({ request }: LoaderFunctionArgs) {
try {
// 获取用户会话信息(服务端需要获取 JWT token)
const { getUserSession } = await import("~/api/login/auth.server");
const { frontendJWT } = await getUserSession(request);
const url = new URL(request.url);
const name = url.searchParams.get('name') || undefined;
const type = url.searchParams.get('type') || undefined;
const status = url.searchParams.get('status') || undefined;
const page = parseInt(url.searchParams.get('page') || '1', 10);
const pageSize = parseInt(url.searchParams.get('pageSize') || '10', 10);
// console.log('加载提示词模板参数:', { name, type: typeParam, status, page, pageSize });
// 从 API 获取数据
const result = await getPromptTemplates({
name,
type,
status,
page,
pageSize
}, frontendJWT);
if (result.error) {
console.error('获取提示词模板失败:', result.error);
return Response.json(
{
templates: [],
total: 0,
pageSize,
currentPage: page,
error: result.error
},
{ status: result.status || 500 }
);
}
// console.log(`成功加载${result.data?.templates.length || 0}条提示词模板数据`);
return Response.json({
templates: result.data?.templates || [],
total: result.data?.total || 0,
pageSize,
currentPage: page
});
} catch (error) {
console.error("加载提示词模板失败:", error);
return Response.json(
{
templates: [],
total: 0,
pageSize: 10,
currentPage: 1,
error: "加载提示词模板失败"
},
{ status: 500 }
);
}
}
// Action函数 - 处理删除请求
export async function action({ request }: ActionFunctionArgs) {
// 获取用户会话信息(服务端需要获取 JWT token)
const { getUserSession } = await import("~/api/login/auth.server");
const { frontendJWT } = await getUserSession(request);
const formData = await request.formData();
const id = formData.get("id") as string;
const intent = formData.get("intent") as string;
if (intent === "delete" && id) {
try {
const result = await deletePromptTemplate(id, frontendJWT);
if (result.error) {
return Response.json({ success: false, error: result.error }, { status: result.status || 500 });
}
return Response.json({ success: true });
} catch (error) {
return Response.json(
{ success: false, error: error instanceof Error ? error.message : "删除提示词模板失败" },
{ status: 500 }
);
}
}
return Response.json({ success: false, error: "无效的操作" }, { status: 400 });
}
// 页面组件
export default function PromptsIndex() {
const navigate = useNavigate();
const [searchParams, setSearchParams] = useSearchParams();
const { templates, total, currentPage, pageSize, error } = useLoaderData<typeof loader>();
const [isLoading, setIsLoading] = useState(false);
const fetcher = useFetcher<ActionData>();
// 🔐 使用新的权限检查Hook
const {
canCreate,
canUpdate,
canDelete,
canView,
hasPermission,
permissions,
userRole
} = usePermission();
// 检查各项权限
const canCreateTemplate = canCreate('prompt_template');
const canEditTemplate = canUpdate('prompt_template');
const canDeleteTemplate = canDelete('prompt_template');
const canViewTemplate = canView('prompt_template');
// 调试信息
useEffect(() => {
console.log('📋 [Prompts] 模板数据:', templates);
// console.log('📋 [Prompts] 用户角色:', userRole);
// console.log('📋 [Prompts] 权限列表:', permissions);
// console.log('📋 [Prompts] 权限检查结果:', {
// canCreate: canCreateTemplate,
// canEdit: canEditTemplate,
// canDelete: canDeleteTemplate,
// canView: canViewTemplate
// });
}, [userRole, permissions, templates, canCreateTemplate, canEditTemplate, canDeleteTemplate, canViewTemplate]);
// 处理搜索名称
const handleNameSearch = (value: string) => {
const newParams = new URLSearchParams(searchParams);
if (value) {
newParams.set('name', value);
} else {
newParams.delete('name');
}
newParams.set('page', '1');
setSearchParams(newParams);
};
// 处理类型筛选变更
const handleTypeChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const { value } = e.target;
const newParams = new URLSearchParams(searchParams);
if (value) {
newParams.set('type', value);
} else {
newParams.delete('type');
}
newParams.set('page', '1');
setSearchParams(newParams);
};
// 处理状态筛选变更
const handleStatusChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const { value } = e.target;
const newParams = new URLSearchParams(searchParams);
if (value) {
newParams.set('status', value);
} else {
newParams.delete('status');
}
newParams.set('page', '1');
setSearchParams(newParams);
};
// 处理重置筛选
const handleReset = () => {
setSearchParams(new URLSearchParams());
};
// 处理搜索按钮点击
// const handleSearch = () => {
// // 搜索已经由 URL 参数变化触发,这里不需要额外操作
// console.log('搜索参数:', Object.fromEntries(searchParams.entries()));
// };
// 查看模板详情
const handleViewTemplate = (id: string) => {
navigate(`/prompts/new?id=${id}&mode=view`);
};
// 复制模板
const handleCloneTemplate = (id: string) => {
if (confirm('确定要复制该模板创建新模板吗?')) {
navigate(`/prompts/new?id=${id}&mode=clone`);
}
};
// 编辑模板
const handleEditTemplate = (id: string) => {
navigate(`/prompts/new?id=${id}&mode=edit`);
};
// 删除模板
const handleDeleteTemplate = (id: string) => {
messageService.show({
title: "确认删除",
message: "确定要删除该模板吗?删除后无法恢复。",
type: "warning",
confirmText: "删除",
cancelText: "取消",
confirmDelay: 4,
onConfirm: () => {
const formData = new FormData();
formData.append('id', id);
formData.append('intent', 'delete');
fetcher.submit(formData, { method: 'post' });
}
});
};
// 监听 loader 错误
useEffect(() => {
if (error) {
toastService.error(error);
}
}, [error]);
// 监听 fetcher 状态变化
useEffect(() => {
if (fetcher.state === 'idle' && fetcher.data) {
if (fetcher.data.success) {
toastService.success('删除成功!');
// Remix 会自动重新验证 loader 数据,无需手动刷新页面
} else if (fetcher.data.error) {
toastService.error(`删除失败: ${fetcher.data.error}`);
}
}
}, [fetcher.state, fetcher.data]);
// 处理分页
const handlePageChange = (page: number) => {
const newParams = new URLSearchParams(searchParams);
newParams.set('page', page.toString());
setSearchParams(newParams);
};
// 处理每页条数变更
const handlePageSizeChange = (size: number) => {
const newParams = new URLSearchParams(searchParams);
newParams.set('pageSize', size.toString());
newParams.set('page', '1'); // 更改每页条数时,重置到第一页
setSearchParams(newParams);
};
// 定义表格列配置
const columns = [
{
title: "模板名称",
key: "template_name",
width: "25%",
render: (_: unknown, record: PromptTemplateUI) => (
<div className="flex items-center">
<i className="ri-file-list-line text-primary mr-2"></i>
<span className="truncate">{record.template_name}</span>
</div>
)
},
{
title: "类型",
key: "template_type",
width: "100px",
render: (_: unknown, record: PromptTemplateUI) => {
let typeText = '';
let typeClass = '';
switch (record.template_type) {
case 'LLM_Extraction':
typeText = '抽取';
typeClass = 'type-extraction';
break;
case 'VLM_Extraction':
typeText = '抽取';
typeClass = 'type-extraction';
break;
case 'Evaluation':
typeText = '评估';
typeClass = 'type-evaluation';
break;
case 'Summary':
typeText = '摘要';
typeClass = 'type-summary';
break;
case 'Common':
typeText = '通用';
typeClass = 'type-common';
break;
}
return <span className={`type-badge ${typeClass}`}>{typeText}</span>;
}
},
{
title: "描述",
key: "description",
width: "30%",
render: (_: unknown, record: PromptTemplateUI) => (
<div className="text-secondary text-sm text-wrap" title={record.description}>
{record.description}
</div>
)
},
{
title: "版本",
key: "version",
width: "70px",
render: (_: unknown, record: PromptTemplateUI) => record.version
},
{
title: "状态",
key: "status",
width: "110px",
render: (_: unknown, record: PromptTemplateUI) => {
let statusText = '';
let statusClass = '';
switch (record.status) {
case 'active':
statusText = '启用';
statusClass = 'status-active';
break;
case 'inactive':
statusText = '停用';
statusClass = 'status-inactive';
break;
case 'system':
statusText = '系统预设';
statusClass = 'status-system';
break;
}
return <span className={`status-badge ${statusClass}`}>{statusText}</span>;
}
},
{
title: "创建者",
key: "created_by",
width: "120px",
render: (_: unknown, record: PromptTemplateUI) => (
// <span className="text-secondary">用户 {record.created_by}</span>
<span className="text-secondary">{record.created_by_username}</span>
)
},
{
title: "操作",
key: "operation",
width: "160px",
render: (_: unknown, record: PromptTemplateUI) => (
<div>
{record.status === 'system' ? (
// 系统预设模板:只能查看,有编辑权限的可以复制
<>
<button
className="operation-btn text-primary"
onClick={() => handleViewTemplate(record.id)}
>
<i className="ri-eye-line"></i>
</button>
{/* 🔐 复制按钮需要创建权限 */}
{canCreateTemplate && (
<button
className="operation-btn text-primary"
onClick={() => handleCloneTemplate(record.id)}
>
<i className="ri-file-copy-line"></i>
</button>
)}
</>
) : (
// 自定义模板:根据权限显示编辑/查看/删除
<>
{/* 🔐 有编辑权限显示编辑按钮,否则显示查看按钮 */}
{canEditTemplate ? (
<button
className="operation-btn text-primary"
onClick={() => handleEditTemplate(record.id)}
>
<i className="ri-edit-line"></i>
</button>
) : canViewTemplate ? (
<button
className="operation-btn text-primary"
onClick={() => handleViewTemplate(record.id)}
>
<i className="ri-eye-line"></i>
</button>
) : null}
{/* 🔐 删除按钮需要删除权限 */}
{canDeleteTemplate && (
<button
className="operation-btn text-error"
onClick={() => handleDeleteTemplate(record.id)}
disabled={isLoading}
>
<i className="ri-delete-bin-line"></i>
</button>
)}
</>
)}
</div>
)
}
];
return (
<div className="prompt-page">
{/* 页面头部 */}
<div className="page-header">
<h2 className="page-title"></h2>
<div>
{/* 🔐 使用权限控制显示新增按钮 */}
<PermissionGuard permission="prompt_template:create:write">
<Button
type="primary"
icon="ri-add-line"
onClick={() => navigate("/prompts/new")}
>
</Button>
</PermissionGuard>
</div>
</div>
{/* 搜索栏 - 使用FilterPanel */}
<FilterPanel
className="mb-4"
actions={
<>
<Button
type="default"
icon="ri-refresh-line"
onClick={handleReset}
className="mr-2"
>
</Button>
{/* <Button
type="primary"
icon="ri-search-line"
onClick={handleSearch}
>
搜索
</Button> */}
</>
}
noActionDivider={true}
>
<SearchFilter
label="模板名称"
placeholder="请输入模板名称"
value={searchParams.get('name') || ''}
onSearch={handleNameSearch}
className="flex-1 min-w-[200px]"
instantSearch={true}
/>
<FilterSelect
label="模板类型"
name="type"
value={searchParams.get('type') || ''}
options={[
{ value: "LLM_Extraction", label: "LLM抽取(LLM_Extraction)" },
{ value: "VLM_Extraction", label: "VLM抽取(VLM_Extraction)" },
{ value: "Evaluation", label: "评估(Evaluation)" },
{ value: "Summary", label: "摘要(Summary)" },
{ value: "Common", label: "通用(Common)" }
]}
onChange={handleTypeChange}
className="flex-1 min-w-[200px]"
/>
<FilterSelect
label="状态"
name="status"
value={searchParams.get('status') || ''}
options={[
{ value: "active", label: "启用" },
{ value: "inactive", label: "停用" },
{ value: "system", label: "系统预设" }
]}
onChange={handleStatusChange}
className="flex-1 min-w-[200px]"
/>
</FilterPanel>
{/* 数据表格 */}
<Card bodyClassName="px-4 py-4">
<Table
columns={columns}
dataSource={templates}
rowKey="id"
emptyText="暂无提示词模板数据"
loading={isLoading}
/>
{/* 分页 */}
<Pagination
currentPage={currentPage}
total={total}
pageSize={pageSize}
onChange={handlePageChange}
onPageSizeChange={handlePageSizeChange}
showTotal={true}
showPageSizeChanger={true}
pageSizeOptions={[10, 20, 30, 50]}
/>
</Card>
</div>
);
}