33f10896a0
2. 接入合同起草功能。
556 lines
17 KiB
TypeScript
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>
|
|
);
|
|
}
|