新增提示词列表和提示词修改页面
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
import React from 'react';
|
||||
|
||||
interface AlertProps {
|
||||
type: 'info' | 'success' | 'warning' | 'error';
|
||||
title: React.ReactNode;
|
||||
className?: string;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export function Alert({ type, title, className = '', children }: AlertProps) {
|
||||
const getTypeStyles = () => {
|
||||
switch (type) {
|
||||
case 'info':
|
||||
return {
|
||||
container: 'bg-blue-50 border-blue-200 text-blue-800',
|
||||
icon: 'ri-information-line text-blue-600'
|
||||
};
|
||||
case 'success':
|
||||
return {
|
||||
container: 'bg-green-50 border-green-200 text-green-800',
|
||||
icon: 'ri-checkbox-circle-line text-green-600'
|
||||
};
|
||||
case 'warning':
|
||||
return {
|
||||
container: 'bg-yellow-50 border-yellow-200 text-yellow-800',
|
||||
icon: 'ri-alert-line text-yellow-600'
|
||||
};
|
||||
case 'error':
|
||||
return {
|
||||
container: 'bg-red-50 border-red-200 text-red-800',
|
||||
icon: 'ri-error-warning-line text-red-600'
|
||||
};
|
||||
default:
|
||||
return {
|
||||
container: 'bg-gray-50 border-gray-200 text-gray-800',
|
||||
icon: 'ri-information-line text-gray-600'
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const styles = getTypeStyles();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`border rounded-md p-3 ${styles.container} ${className}`}
|
||||
role="alert"
|
||||
>
|
||||
<div className="flex">
|
||||
<div className="flex-shrink-0">
|
||||
<i className={`${styles.icon} mr-2 text-lg`}></i>
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-medium">{title}</div>
|
||||
{children && <div className="mt-1 text-sm">{children}</div>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -13,9 +13,6 @@ export const links = () => [
|
||||
{ rel: "stylesheet", href: configListsStyles }
|
||||
];
|
||||
|
||||
// export const handle = {
|
||||
// breadcrumb: "系统配置管理"
|
||||
// };
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return [
|
||||
|
||||
@@ -11,9 +11,8 @@ export const links = () => [
|
||||
];
|
||||
|
||||
export const handle = {
|
||||
breadcrumb: ({ location }: { location: Location }) => {
|
||||
const hasId = new URLSearchParams(location.search).has("id");
|
||||
return hasId ? "编辑配置" : "新增配置";
|
||||
breadcrumb: (data:LoaderData) => {
|
||||
return data.isEdit ? "编辑配置" : "新增配置";
|
||||
}
|
||||
};
|
||||
|
||||
@@ -159,7 +158,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
}
|
||||
}
|
||||
|
||||
return json<LoaderData>({
|
||||
return Response.json({
|
||||
config,
|
||||
isEdit: !!config
|
||||
});
|
||||
@@ -439,19 +438,10 @@ export default function ConfigNew() {
|
||||
<i className="ri-arrow-left-line mr-1"></i>
|
||||
返回
|
||||
</Button>
|
||||
<Form method="post" className="inline">
|
||||
{config?.id && <input type="hidden" name="id" value={config.id} />}
|
||||
<input type="hidden" name="configName" value={config?.configName || ''} />
|
||||
<input type="hidden" name="module" value={selectedModule} />
|
||||
<input type="hidden" name="environment" value={selectedEnvironment} />
|
||||
<input type="hidden" name="configData" value={configDataValue} />
|
||||
<input type="hidden" name="isActive" value={config?.isActive !== false ? "true" : "false"} />
|
||||
<input type="hidden" name="remarks" value={config?.remarks || ''} />
|
||||
<Button type="primary" disabled={isSubmitting}>
|
||||
<i className="ri-save-line mr-1"></i>
|
||||
{isSubmitting ? '保存中...' : '保存'}
|
||||
</Button>
|
||||
</Form>
|
||||
<Button type="primary" disabled={isSubmitting} form="configForm">
|
||||
<i className="ri-save-line mr-1"></i>
|
||||
{isSubmitting ? '保存中...' : '保存'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -0,0 +1,479 @@
|
||||
import { MetaFunction } from "@remix-run/node";
|
||||
import { useSearchParams, useNavigate } from "@remix-run/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";
|
||||
|
||||
// 定义提示词模板类型
|
||||
export interface PromptTemplate {
|
||||
id: string;
|
||||
template_name: string;
|
||||
template_type: 'Common' | 'Extraction' | 'Evaluation' | 'Summary';
|
||||
description: string;
|
||||
version: string;
|
||||
status: 'active' | 'inactive' | 'system';
|
||||
created_by: string;
|
||||
template_content: string;
|
||||
variables: string; // JSON字符串
|
||||
}
|
||||
|
||||
// 样式链接
|
||||
export function links() {
|
||||
return [{ rel: "stylesheet", href: indexStyles }];
|
||||
}
|
||||
|
||||
// 页面元数据
|
||||
export const meta: MetaFunction = () => {
|
||||
return [
|
||||
{ title: "提示词模板管理 - 中国烟草AI合同及卷宗审核系统" },
|
||||
{ name: "description", content: "管理提示词模板,包括创建、编辑和删除提示词模板" },
|
||||
];
|
||||
};
|
||||
|
||||
// 面包屑导航
|
||||
export const handle = {
|
||||
breadcrumb: "提示词模板管理"
|
||||
};
|
||||
|
||||
// 模拟数据
|
||||
const MOCK_TEMPLATES: PromptTemplate[] = [
|
||||
{
|
||||
id: "1",
|
||||
template_name: "行政处罚-抽取通用模板",
|
||||
template_type: "Extraction",
|
||||
description: "本模板用于抽取行政处罚决定书编号等信息",
|
||||
version: "v1.0",
|
||||
status: "system",
|
||||
created_by: "system",
|
||||
template_content: `你是一个专业的文档信息抽取助手。请从以下{docType}文档中抽取关键信息:
|
||||
1. 处罚决定书编号
|
||||
2. 处罚对象名称
|
||||
3. 处罚事由
|
||||
4. 处罚依据
|
||||
5. 处罚内容
|
||||
6. 处罚金额
|
||||
7. 发文日期
|
||||
请将结果以JSON格式输出,包含以上字段。如果某个字段在文档中未找到,则该字段的值设为null。`,
|
||||
variables: JSON.stringify({ "docType": "文档类型" })
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
template_name: "销售合同-甲方信息评估",
|
||||
template_type: "Evaluation",
|
||||
description: "评估销售合同中甲方信息是否完整",
|
||||
version: "v1.2",
|
||||
status: "active",
|
||||
created_by: "admin",
|
||||
template_content: `你是一个专业的合同审核助手。请评估以下{docType}中甲方信息的完整性:
|
||||
请检查以下要素是否存在且完整:
|
||||
1. 甲方全称
|
||||
2. 注册地址
|
||||
3. 统一社会信用代码
|
||||
4. 法定代表人
|
||||
5. 联系方式
|
||||
请给出评估结果,并标明缺失或不完整的信息。`,
|
||||
variables: JSON.stringify({ "docType": "文档类型" })
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
template_name: "专卖许可证-摘要模板",
|
||||
template_type: "Summary",
|
||||
description: "生成专卖许可证申请文件的内容摘要",
|
||||
version: "v1.0",
|
||||
status: "active",
|
||||
created_by: "admin",
|
||||
template_content: `你是一个专业的文档摘要助手。请为以下{docType}生成一份简洁的摘要:
|
||||
摘要应包含以下要点:
|
||||
1. 申请人基本信息
|
||||
2. 许可证类型
|
||||
3. 申请事项
|
||||
4. 经营范围
|
||||
5. 申请日期
|
||||
请控制摘要在200字以内,保留关键信息。`,
|
||||
variables: JSON.stringify({ "docType": "文档类型" })
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
template_name: "采购合同-乙方资质抽取",
|
||||
template_type: "Extraction",
|
||||
description: "抽取采购合同中乙方的资质信息",
|
||||
version: "v1.1",
|
||||
status: "inactive",
|
||||
created_by: "zhangsan",
|
||||
template_content: `你是一个专业的合同信息抽取助手。请从以下{docType}中抽取乙方的资质信息:
|
||||
需要抽取的信息包括:
|
||||
1. 乙方全称
|
||||
2. 资质证书类型
|
||||
3. 资质证书编号
|
||||
4. 资质等级
|
||||
5. 证书有效期
|
||||
请将结果以JSON格式输出,包含以上字段。`,
|
||||
variables: JSON.stringify({ "docType": "文档类型" })
|
||||
},
|
||||
{
|
||||
id: "5",
|
||||
template_name: "合同通用-关键条款评估",
|
||||
template_type: "Evaluation",
|
||||
description: "评估合同中关键条款是否明确、合规",
|
||||
version: "v2.0",
|
||||
status: "active",
|
||||
created_by: "lisi",
|
||||
template_content: `你是一个专业的{industry}行业合同审核助手。请评估以下合同中的关键条款是否明确、合规:
|
||||
请重点关注以下条款:
|
||||
1. 合同标的
|
||||
2. 价格条款
|
||||
3. 付款条件
|
||||
4. 交付方式
|
||||
5. 违约责任
|
||||
6. 争议解决
|
||||
请对每一项给出评估结果,并指出不明确或存在风险的条款。`,
|
||||
variables: JSON.stringify({ "industry": "行业类型", "docType": "文档类型" })
|
||||
}
|
||||
];
|
||||
|
||||
// 数据加载器
|
||||
export async function loader() {
|
||||
try {
|
||||
// 实际应用中,这里应该调用API获取数据
|
||||
// const response = await fetch(`${process.env.API_BASE_URL}/api/prompt-templates`);
|
||||
// if (!response.ok) throw new Error(`获取提示词模板失败: ${response.status}`);
|
||||
// const templates = await response.json();
|
||||
|
||||
// 使用模拟数据
|
||||
const templates = MOCK_TEMPLATES;
|
||||
|
||||
return Response.json({
|
||||
templates,
|
||||
total: templates.length,
|
||||
pageSize: 10,
|
||||
currentPage: 1
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("加载提示词模板失败:", error);
|
||||
return Response.json(
|
||||
{
|
||||
templates: [],
|
||||
total: 0,
|
||||
pageSize: 10,
|
||||
currentPage: 1,
|
||||
error: "加载提示词模板失败"
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 页面组件
|
||||
export default function PromptsIndex() {
|
||||
const navigate = useNavigate();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
|
||||
// 处理搜索名称
|
||||
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 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) => {
|
||||
if (confirm('确定要删除该模板吗?删除后无法恢复。')) {
|
||||
// 实际应该调用API删除数据
|
||||
console.log('删除模板ID:', id);
|
||||
alert('删除成功!');
|
||||
// 刷新页面
|
||||
window.location.reload();
|
||||
}
|
||||
};
|
||||
|
||||
// 定义表格列配置
|
||||
const columns = [
|
||||
{
|
||||
title: "模板名称",
|
||||
key: "template_name",
|
||||
width: "300px",
|
||||
render: (_: unknown, record: PromptTemplate) => (
|
||||
<div className="flex items-center">
|
||||
<i className="ri-file-list-line text-primary mr-2"></i>
|
||||
<span>{record.template_name}</span>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: "类型",
|
||||
key: "template_type",
|
||||
width: "100px",
|
||||
render: (_: unknown, record: PromptTemplate) => {
|
||||
let typeText = '';
|
||||
let typeClass = '';
|
||||
|
||||
switch (record.template_type) {
|
||||
case '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",
|
||||
render: (_: unknown, record: PromptTemplate) => (
|
||||
<div className="text-secondary text-sm truncate max-w-xs" title={record.description}>
|
||||
{record.description}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: "版本",
|
||||
key: "version",
|
||||
width: "80px",
|
||||
render: (_: unknown, record: PromptTemplate) => record.version
|
||||
},
|
||||
{
|
||||
title: "状态",
|
||||
key: "status",
|
||||
width: "80px",
|
||||
render: (_: unknown, record: PromptTemplate) => {
|
||||
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: "100px",
|
||||
render: (_: unknown, record: PromptTemplate) => record.created_by
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
key: "operation",
|
||||
width: "150px",
|
||||
render: (_: unknown, record: PromptTemplate) => (
|
||||
<div>
|
||||
{record.status === 'system' ? (
|
||||
<>
|
||||
<button
|
||||
className="operation-btn text-primary"
|
||||
onClick={() => handleViewTemplate(record.id)}
|
||||
>
|
||||
<i className="ri-eye-line"></i> 查看
|
||||
</button>
|
||||
<button
|
||||
className="operation-btn text-primary"
|
||||
onClick={() => handleCloneTemplate(record.id)}
|
||||
>
|
||||
<i className="ri-file-copy-line"></i> 复制
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<button
|
||||
className="operation-btn text-primary"
|
||||
onClick={() => handleEditTemplate(record.id)}
|
||||
>
|
||||
<i className="ri-edit-line"></i> 编辑
|
||||
</button>
|
||||
<button
|
||||
className="operation-btn text-error"
|
||||
onClick={() => handleDeleteTemplate(record.id)}
|
||||
>
|
||||
<i className="ri-delete-bin-line"></i> 删除
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="prompt-page">
|
||||
{/* 页面头部 */}
|
||||
<div className="page-header">
|
||||
<h2 className="page-title">提示词模板管理</h2>
|
||||
<div>
|
||||
<Button
|
||||
type="primary"
|
||||
icon="ri-add-line"
|
||||
onClick={() => navigate("/prompts/new")}
|
||||
>
|
||||
新增提示词模板
|
||||
</Button>
|
||||
</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"
|
||||
>
|
||||
搜索
|
||||
</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: "", label: "全部" },
|
||||
{ value: "Extraction", label: "抽取(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: "", label: "全部" },
|
||||
{ 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={MOCK_TEMPLATES}
|
||||
rowKey="id"
|
||||
emptyText="暂无提示词模板数据"
|
||||
/>
|
||||
|
||||
{/* 分页 */}
|
||||
<Pagination
|
||||
currentPage={1}
|
||||
total={MOCK_TEMPLATES.length}
|
||||
pageSize={10}
|
||||
onChange={(page) => {
|
||||
const newParams = new URLSearchParams(searchParams);
|
||||
newParams.set('page', page.toString());
|
||||
setSearchParams(newParams);
|
||||
}}
|
||||
showTotal={true}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,711 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { MetaFunction, LoaderFunctionArgs, ActionFunctionArgs } from "@remix-run/node";
|
||||
import { Link, useLoaderData, useSubmit, useNavigation } from "@remix-run/react";
|
||||
import { Button } from "~/components/ui/Button";
|
||||
import { PromptTemplate } from "./prompts._index";
|
||||
import newStyles from "~/styles/pages/prompts_new.css?url";
|
||||
|
||||
// 样式链接
|
||||
export function links() {
|
||||
return [{ rel: "stylesheet", href: newStyles }];
|
||||
}
|
||||
|
||||
// 页面元数据
|
||||
export const meta: MetaFunction = () => {
|
||||
return [
|
||||
{ title: "提示词模板编辑 - 中国烟草AI合同及卷宗审核系统" },
|
||||
{ name: "description", content: "创建或编辑提示词模板" },
|
||||
];
|
||||
};
|
||||
|
||||
// 面包屑导航
|
||||
export const handle = {
|
||||
breadcrumb: "编辑提示词模板"
|
||||
};
|
||||
|
||||
// 从模拟数据中获取模板
|
||||
const getTemplateById = (id: string): PromptTemplate | undefined => {
|
||||
// 与prompts._index.tsx中的模拟数据保持一致
|
||||
const MOCK_TEMPLATES: PromptTemplate[] = [
|
||||
{
|
||||
id: "1",
|
||||
template_name: "行政处罚-抽取通用模板",
|
||||
template_type: "Extraction",
|
||||
description: "本模板用于抽取行政处罚决定书编号等信息",
|
||||
version: "v1.0",
|
||||
status: "system",
|
||||
created_by: "system",
|
||||
template_content: `你是一个专业的文档信息抽取助手。请从以下{docType}文档中抽取关键信息:
|
||||
1. 处罚决定书编号
|
||||
2. 处罚对象名称
|
||||
3. 处罚事由
|
||||
4. 处罚依据
|
||||
5. 处罚内容
|
||||
6. 处罚金额
|
||||
7. 发文日期
|
||||
请将结果以JSON格式输出,包含以上字段。如果某个字段在文档中未找到,则该字段的值设为null。`,
|
||||
variables: JSON.stringify({ "docType": "文档类型" })
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
template_name: "销售合同-甲方信息评估",
|
||||
template_type: "Evaluation",
|
||||
description: "评估销售合同中甲方信息是否完整",
|
||||
version: "v1.2",
|
||||
status: "active",
|
||||
created_by: "admin",
|
||||
template_content: `你是一个专业的合同审核助手。请评估以下{docType}中甲方信息的完整性:
|
||||
请检查以下要素是否存在且完整:
|
||||
1. 甲方全称
|
||||
2. 注册地址
|
||||
3. 统一社会信用代码
|
||||
4. 法定代表人
|
||||
5. 联系方式
|
||||
请给出评估结果,并标明缺失或不完整的信息。`,
|
||||
variables: JSON.stringify({ "docType": "文档类型" })
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
template_name: "专卖许可证-摘要模板",
|
||||
template_type: "Summary",
|
||||
description: "生成专卖许可证申请文件的内容摘要",
|
||||
version: "v1.0",
|
||||
status: "active",
|
||||
created_by: "admin",
|
||||
template_content: `你是一个专业的文档摘要助手。请为以下{docType}生成一份简洁的摘要:
|
||||
摘要应包含以下要点:
|
||||
1. 申请人基本信息
|
||||
2. 许可证类型
|
||||
3. 申请事项
|
||||
4. 经营范围
|
||||
5. 申请日期
|
||||
请控制摘要在200字以内,保留关键信息。`,
|
||||
variables: JSON.stringify({ "docType": "文档类型" })
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
template_name: "采购合同-乙方资质抽取",
|
||||
template_type: "Extraction",
|
||||
description: "抽取采购合同中乙方的资质信息",
|
||||
version: "v1.1",
|
||||
status: "inactive",
|
||||
created_by: "zhangsan",
|
||||
template_content: `你是一个专业的合同信息抽取助手。请从以下{docType}中抽取乙方的资质信息:
|
||||
需要抽取的信息包括:
|
||||
1. 乙方全称
|
||||
2. 资质证书类型
|
||||
3. 资质证书编号
|
||||
4. 资质等级
|
||||
5. 证书有效期
|
||||
请将结果以JSON格式输出,包含以上字段。`,
|
||||
variables: JSON.stringify({ "docType": "文档类型" })
|
||||
},
|
||||
{
|
||||
id: "5",
|
||||
template_name: "合同通用-关键条款评估",
|
||||
template_type: "Evaluation",
|
||||
description: "评估合同中关键条款是否明确、合规",
|
||||
version: "v2.0",
|
||||
status: "active",
|
||||
created_by: "lisi",
|
||||
template_content: `你是一个专业的{industry}行业合同审核助手。请评估以下合同中的关键条款是否明确、合规:
|
||||
请重点关注以下条款:
|
||||
1. 合同标的
|
||||
2. 价格条款
|
||||
3. 付款条件
|
||||
4. 交付方式
|
||||
5. 违约责任
|
||||
6. 争议解决
|
||||
请对每一项给出评估结果,并指出不明确或存在风险的条款。`,
|
||||
variables: JSON.stringify({ "industry": "行业类型", "docType": "文档类型" })
|
||||
}
|
||||
];
|
||||
|
||||
return MOCK_TEMPLATES.find(t => t.id === id);
|
||||
};
|
||||
|
||||
// 加载函数
|
||||
export async function loader({ request }: LoaderFunctionArgs) {
|
||||
try {
|
||||
const url = new URL(request.url);
|
||||
const id = url.searchParams.get("id");
|
||||
const mode = url.searchParams.get("mode") || "create";
|
||||
|
||||
// 模板数据,如果是新建则为空
|
||||
let template = null;
|
||||
|
||||
if (id) {
|
||||
// 实际应用中,这里应该调用API获取数据
|
||||
// const response = await fetch(`${process.env.API_BASE_URL}/api/prompt-templates/${id}`);
|
||||
// if (!response.ok) throw new Error(`获取提示词模板失败: ${response.status}`);
|
||||
// template = await response.json();
|
||||
|
||||
// 使用模拟数据
|
||||
template = getTemplateById(id);
|
||||
if (!template) {
|
||||
throw new Error(`未找到ID为${id}的模板`);
|
||||
}
|
||||
}
|
||||
|
||||
return Response.json({
|
||||
template,
|
||||
mode
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("加载提示词模板失败:", error);
|
||||
return Response.json(
|
||||
{
|
||||
template: null,
|
||||
mode: "create",
|
||||
error: error instanceof Error ? error.message : "加载提示词模板失败"
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Action函数 - 处理表单提交
|
||||
export async function action({ request }: ActionFunctionArgs) {
|
||||
try {
|
||||
const formData = await request.formData();
|
||||
const templateData = Object.fromEntries(formData);
|
||||
|
||||
// 表单验证
|
||||
const errors: Record<string, string> = {};
|
||||
|
||||
if (!templateData.template_name) {
|
||||
errors.template_name = "模板名称不能为空";
|
||||
}
|
||||
|
||||
if (!templateData.template_type) {
|
||||
errors.template_type = "请选择模板类型";
|
||||
}
|
||||
|
||||
if (!templateData.template_content) {
|
||||
errors.template_content = "模板内容不能为空";
|
||||
}
|
||||
|
||||
if (Object.keys(errors).length > 0) {
|
||||
return Response.json({ errors, success: false }, { status: 400 });
|
||||
}
|
||||
|
||||
// 实际应用中,这里应该调用API保存数据
|
||||
// const apiUrl = templateData.id
|
||||
// ? `${process.env.API_BASE_URL}/api/prompt-templates/${templateData.id}`
|
||||
// : `${process.env.API_BASE_URL}/api/prompt-templates`;
|
||||
//
|
||||
// const response = await fetch(apiUrl, {
|
||||
// method: templateData.id ? "PUT" : "POST",
|
||||
// headers: { "Content-Type": "application/json" },
|
||||
// body: JSON.stringify(templateData)
|
||||
// });
|
||||
//
|
||||
// if (!response.ok) throw new Error(`保存提示词模板失败: ${response.status}`);
|
||||
// const result = await response.json();
|
||||
|
||||
// 模拟API响应
|
||||
console.log("提交的模板数据:", templateData);
|
||||
|
||||
return Response.json({
|
||||
success: true,
|
||||
message: "提示词模板保存成功",
|
||||
template: {
|
||||
...templateData,
|
||||
id: templateData.id || Math.random().toString(36).substring(2, 10),
|
||||
created_by: "当前用户",
|
||||
created_at: new Date().toISOString()
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("保存提示词模板失败:", error);
|
||||
return Response.json(
|
||||
{
|
||||
success: false,
|
||||
message: error instanceof Error ? error.message : "保存提示词模板失败"
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 提取变量函数
|
||||
const extractVariables = (content: string) => {
|
||||
const regex = /{([^{}]+)}/g;
|
||||
const variables: Record<string, string> = {};
|
||||
let match;
|
||||
|
||||
while ((match = regex.exec(content)) !== null) {
|
||||
const varName = match[1].trim();
|
||||
if (varName && !variables[varName]) {
|
||||
variables[varName] = varName;
|
||||
}
|
||||
}
|
||||
|
||||
return variables;
|
||||
};
|
||||
|
||||
// 页面组件
|
||||
export default function PromptsNew() {
|
||||
const { template, mode } = useLoaderData<typeof loader>();
|
||||
const submit = useSubmit();
|
||||
const navigation = useNavigation();
|
||||
const isSubmitting = navigation.state === "submitting";
|
||||
|
||||
// 表单状态
|
||||
const [formData, setFormData] = useState<Partial<PromptTemplate>>({
|
||||
id: "",
|
||||
template_name: "",
|
||||
template_type: "Common",
|
||||
description: "",
|
||||
version: "v1.0",
|
||||
status: "active",
|
||||
template_content: "",
|
||||
variables: "{}"
|
||||
});
|
||||
|
||||
// 模式状态
|
||||
const [isViewMode, setIsViewMode] = useState(false);
|
||||
const [pageTitle, setPageTitle] = useState("新增提示词模板");
|
||||
|
||||
// 变量相关状态
|
||||
const [detectedVariables, setDetectedVariables] = useState<Record<string, string>>({});
|
||||
const [exampleValues, setExampleValues] = useState<Record<string, string>>({});
|
||||
const [previewContent, setPreviewContent] = useState("");
|
||||
|
||||
// 初始化表单数据
|
||||
useEffect(() => {
|
||||
if (template) {
|
||||
const newFormData = {
|
||||
...template,
|
||||
// 如果是克隆模式,则清除ID并修改名称
|
||||
id: mode === "clone" ? "" : template.id,
|
||||
template_name: mode === "clone" ? `${template.template_name} (副本)` : template.template_name,
|
||||
// 如果是克隆模式,重置版本
|
||||
version: mode === "clone" ? "v1.0" : template.version
|
||||
};
|
||||
|
||||
setFormData(newFormData);
|
||||
|
||||
try {
|
||||
// 解析模板变量
|
||||
const vars = JSON.parse(template.variables);
|
||||
setExampleValues(vars);
|
||||
} catch (e) {
|
||||
console.error("解析变量失败:", e);
|
||||
}
|
||||
|
||||
// 检测模板内容中的变量
|
||||
if (template.template_content) {
|
||||
const vars = extractVariables(template.template_content);
|
||||
setDetectedVariables(vars);
|
||||
}
|
||||
}
|
||||
|
||||
// 设置页面模式
|
||||
setIsViewMode(mode === "view");
|
||||
|
||||
// 设置页面标题
|
||||
if (mode === "view") {
|
||||
setPageTitle("查看提示词模板");
|
||||
} else if (mode === "edit") {
|
||||
setPageTitle("编辑提示词模板");
|
||||
} else if (mode === "clone") {
|
||||
setPageTitle("复制创建提示词模板");
|
||||
} else {
|
||||
setPageTitle("新增提示词模板");
|
||||
}
|
||||
}, [template, mode]);
|
||||
|
||||
// 处理输入变化
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
|
||||
const { name, value } = e.target;
|
||||
|
||||
// 如果是模板内容,检测变量
|
||||
if (name === "template_content") {
|
||||
const vars = extractVariables(value);
|
||||
setDetectedVariables(vars);
|
||||
|
||||
// 更新变量JSON
|
||||
const varsJson = JSON.stringify(
|
||||
Object.keys(vars).reduce((acc, key) => {
|
||||
acc[key] = exampleValues[key] || key;
|
||||
return acc;
|
||||
}, {} as Record<string, string>)
|
||||
);
|
||||
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
[name]: value,
|
||||
variables: varsJson
|
||||
}));
|
||||
} else {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
[name]: value
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
// 处理状态切换
|
||||
const handleStatusToggle = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const status = e.target.checked ? "active" : "inactive";
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
status
|
||||
}));
|
||||
};
|
||||
|
||||
// 处理示例值变更
|
||||
const handleExampleValueChange = (varName: string, value: string) => {
|
||||
setExampleValues(prev => ({
|
||||
...prev,
|
||||
[varName]: value
|
||||
}));
|
||||
};
|
||||
|
||||
// 更新预览
|
||||
const updatePreview = () => {
|
||||
let content = formData.template_content || "";
|
||||
|
||||
// 替换变量
|
||||
Object.entries(detectedVariables).forEach(([key]) => {
|
||||
const exampleValue = exampleValues[key] || `[${key}]`;
|
||||
const regex = new RegExp(`{${key}}`, 'g');
|
||||
content = content.replace(regex, exampleValue);
|
||||
});
|
||||
|
||||
setPreviewContent(content);
|
||||
};
|
||||
|
||||
// 当检测到的变量变化时,更新变量JSON
|
||||
useEffect(() => {
|
||||
const varsJson = JSON.stringify(
|
||||
Object.keys(detectedVariables).reduce((acc, key) => {
|
||||
acc[key] = exampleValues[key] || key;
|
||||
return acc;
|
||||
}, {} as Record<string, string>)
|
||||
);
|
||||
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
variables: varsJson
|
||||
}));
|
||||
}, [detectedVariables, exampleValues]);
|
||||
|
||||
// 处理表单提交
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (isViewMode) {
|
||||
return;
|
||||
}
|
||||
|
||||
const formElement = e.target as HTMLFormElement;
|
||||
const submittingFormData = new FormData(formElement);
|
||||
|
||||
// 确保变量JSON被包含在提交中
|
||||
submittingFormData.set("variables", formData.variables || "{}");
|
||||
|
||||
submit(submittingFormData, { method: "post" });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="prompt-new-page">
|
||||
{/* 页面头部 */}
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h2 className="text-xl font-medium">{pageTitle}</h2>
|
||||
<div>
|
||||
<Link to="/prompts" className="mr-2">
|
||||
<Button type="default" icon="ri-arrow-left-line">
|
||||
返回
|
||||
</Button>
|
||||
</Link>
|
||||
{!isViewMode ? (
|
||||
<Button
|
||||
type="primary"
|
||||
icon="ri-save-line"
|
||||
disabled={isSubmitting}
|
||||
onClick={() => document.getElementById("template-form")?.dispatchEvent(new Event("submit", { cancelable: true, bubbles: true }))}
|
||||
>
|
||||
{isSubmitting ? "保存中..." : "保存"}
|
||||
</Button>
|
||||
) : (
|
||||
<Link to="/prompts">
|
||||
<Button type="default" icon="ri-arrow-left-line">
|
||||
返回列表
|
||||
</Button>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 查看模式提示 */}
|
||||
{isViewMode && (
|
||||
<div className="alert alert-info">
|
||||
<i className="ri-information-line"></i>
|
||||
<div>
|
||||
<div>您正在查看系统预设模板,此模板不可修改。如需基于此模板创建新模板,请点击"复制创建"按钮。</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 模板表单 */}
|
||||
<form id="template-form" method="post" onSubmit={handleSubmit}>
|
||||
{/* 模板ID - 隐藏字段 */}
|
||||
<input type="hidden" name="id" value={formData.id || ''} />
|
||||
|
||||
{/* 模板信息卡片 */}
|
||||
<div className="ant-card">
|
||||
<div className="ant-card-header">提示词模板信息</div>
|
||||
<div className="ant-card-body p-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{/* 模板名称 */}
|
||||
<div className="form-group mb-3">
|
||||
<label htmlFor="template-name" className="form-label mb-1">
|
||||
模板名称 <span className="text-error">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
className={`form-input py-1 ${isViewMode ? 'read-only-field' : ''}`}
|
||||
id="template-name"
|
||||
name="template_name"
|
||||
placeholder="请输入模板名称"
|
||||
value={formData.template_name || ''}
|
||||
onChange={handleInputChange}
|
||||
readOnly={isViewMode}
|
||||
required
|
||||
/>
|
||||
<div className="help-text text-xs">建议使用"文档类型-功能"的命名方式</div>
|
||||
</div>
|
||||
|
||||
{/* 模板类型 */}
|
||||
<div className="form-group mb-3">
|
||||
<label htmlFor="template-type" className="form-label mb-1">
|
||||
模板类型 <span className="text-error">*</span>
|
||||
</label>
|
||||
<select
|
||||
className={`form-select py-1 ${isViewMode ? 'read-only-field' : ''}`}
|
||||
id="template-type"
|
||||
name="template_type"
|
||||
value={formData.template_type || ''}
|
||||
onChange={handleInputChange}
|
||||
disabled={isViewMode}
|
||||
required
|
||||
>
|
||||
<option value="">请选择模板类型</option>
|
||||
<option value="Common">通用(Common) - 适用于多种场景的通用提示词</option>
|
||||
<option value="Extraction">抽取(Extraction) - 从文档抽取结构化信息</option>
|
||||
<option value="Evaluation">评估(Evaluation) - 对文档内容进行评估</option>
|
||||
<option value="Summary">摘要(Summary) - 生成文档内容摘要</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* 模板描述 */}
|
||||
<div className="form-group mb-3">
|
||||
<label htmlFor="template-description" className="form-label mb-1">
|
||||
模板描述
|
||||
</label>
|
||||
<textarea
|
||||
className={`form-textarea py-1 ${isViewMode ? 'read-only-field' : ''}`}
|
||||
id="template-description"
|
||||
name="description"
|
||||
placeholder="请简要描述此模板的功能和用途"
|
||||
value={formData.description || ''}
|
||||
onChange={handleInputChange}
|
||||
readOnly={isViewMode}
|
||||
rows={2}
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{/* 模板状态 */}
|
||||
<div className="form-group mb-3">
|
||||
<label htmlFor="status-toggle" className="form-label mb-1">模板状态</label>
|
||||
<div className="flex items-center mt-1">
|
||||
<label className="switch mr-2" htmlFor="status-toggle">
|
||||
<span className="sr-only">切换模板状态</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
id="status-toggle"
|
||||
name="status"
|
||||
checked={formData.status === 'active'}
|
||||
onChange={handleStatusToggle}
|
||||
disabled={isViewMode}
|
||||
/>
|
||||
<span className="slider"></span>
|
||||
</label>
|
||||
<span id="status-text">{formData.status === 'active' ? '启用' : '停用'}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 模板版本 */}
|
||||
<div className="form-group mb-3">
|
||||
<label htmlFor="template-version" className="form-label mb-1">模板版本</label>
|
||||
<input
|
||||
type="text"
|
||||
className={`form-input py-1 ${isViewMode ? 'read-only-field' : ''}`}
|
||||
id="template-version"
|
||||
name="version"
|
||||
placeholder="例如:v1.0"
|
||||
value={formData.version || 'v1.0'}
|
||||
onChange={handleInputChange}
|
||||
readOnly={isViewMode}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 模板内容卡片 */}
|
||||
<div className="ant-card">
|
||||
<div className="ant-card-header">提示词模板内容</div>
|
||||
<div className="ant-card-body">
|
||||
<div className="alert alert-warning mb-4">
|
||||
<i className="ri-information-line"></i>
|
||||
<div>
|
||||
<div>模板内容支持使用变量,变量格式为 <code>{"{varName}"}</code>,在使用时会自动替换。系统将自动识别模板中的变量。</div>
|
||||
<div className="mt-1">例如:<code>请从以下{"{docType}"}文档中抽取关键信息...</code></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 模板内容 */}
|
||||
<div className="form-group">
|
||||
<label htmlFor="template-content" className="form-label">
|
||||
模板内容 <span className="text-error">*</span>
|
||||
</label>
|
||||
<textarea
|
||||
className={`form-code-editor w-full ${isViewMode ? 'read-only-field' : ''}`}
|
||||
id="template-content"
|
||||
name="template_content"
|
||||
placeholder="在此输入提示词模板内容..."
|
||||
value={formData.template_content || ''}
|
||||
onChange={handleInputChange}
|
||||
readOnly={isViewMode}
|
||||
rows={15}
|
||||
required
|
||||
></textarea>
|
||||
<div className="help-text">提示词模板是AI完成特定任务的指令,请清晰描述任务需求和输出格式</div>
|
||||
</div>
|
||||
|
||||
{/* 变量识别区域 */}
|
||||
<div className="form-group mt-6">
|
||||
<label htmlFor="var-container" className="form-label">已识别的变量</label>
|
||||
<div className="alert alert-info mb-3">
|
||||
<i className="ri-lightbulb-line"></i>
|
||||
<div>
|
||||
<div>系统已自动识别出模板中的变量。变量以 <code>{"{varName}"}</code> 形式在模板中使用,无需手动定义。</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="var-container" className="var-container" aria-labelledby="var-container-label">
|
||||
{Object.keys(detectedVariables).length > 0 ? (
|
||||
Object.keys(detectedVariables).map(varName => (
|
||||
<div
|
||||
className="var-badge"
|
||||
key={varName}
|
||||
data-var-name={varName}
|
||||
>
|
||||
{varName}
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="text-secondary text-sm italic" id="no-vars-message">
|
||||
暂未识别到任何变量,请在模板内容中使用 {"{变量名}"} 格式添加变量
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<input type="hidden" id="variables-json" name="variables" value={formData.variables || '{}'} />
|
||||
</div>
|
||||
|
||||
{/* 模板效果预览 */}
|
||||
<div className="form-group mt-6">
|
||||
<label htmlFor="preview-content" className="form-label">模板效果预览</label>
|
||||
<div className="example-section">
|
||||
<div className="example-header">变量赋值示例:</div>
|
||||
<div id="example-vars" className="mb-3">
|
||||
{Object.keys(detectedVariables).length > 0 ? (
|
||||
Object.keys(detectedVariables).map(varName => (
|
||||
<div className="var-input-group" key={varName}>
|
||||
<input
|
||||
type="text"
|
||||
className="form-input"
|
||||
value={varName}
|
||||
readOnly
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
className={`form-input ${isViewMode ? 'read-only-field' : ''}`}
|
||||
id={`example-${varName}`}
|
||||
placeholder={`示例值,如 ${varName}`}
|
||||
value={exampleValues[varName] || ''}
|
||||
onChange={(e) => handleExampleValueChange(varName, e.target.value)}
|
||||
readOnly={isViewMode}
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="text-secondary text-sm italic">
|
||||
暂无变量,无需赋值
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="example-header">预览效果:</div>
|
||||
<div id="preview-content" className="bg-gray-50 p-4 rounded border border-gray-200">
|
||||
{previewContent ? (
|
||||
<pre style={{ whiteSpace: 'pre-wrap' }}>{previewContent}</pre>
|
||||
) : (
|
||||
<div className="text-gray-400 italic">根据变量值生成的模板预览将显示在这里...</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end mt-3">
|
||||
<button
|
||||
type="button"
|
||||
className="ant-btn ant-btn-default"
|
||||
onClick={updatePreview}
|
||||
>
|
||||
<i className="ri-eye-line"></i> 更新预览
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 底部按钮区域 */}
|
||||
<div className="flex justify-between mt-6">
|
||||
<div>
|
||||
{isViewMode && (
|
||||
<Link to={`/prompts/new?id=${formData.id}&mode=clone`}>
|
||||
<button type="button" className="ant-btn ant-btn-default">
|
||||
<i className="ri-file-copy-line"></i> 复制创建
|
||||
</button>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<Link to="/prompts" className="mr-2">
|
||||
<button type="button" className="ant-btn ant-btn-default">
|
||||
<i className="ri-close-line"></i> 取消
|
||||
</button>
|
||||
</Link>
|
||||
{!isViewMode && (
|
||||
<button
|
||||
type="submit"
|
||||
className="ant-btn ant-btn-primary"
|
||||
disabled={isSubmitting}
|
||||
id="save-btn-bottom"
|
||||
>
|
||||
<i className="ri-save-line"></i> {isSubmitting ? "保存中..." : "保存"}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Outlet } from "@remix-run/react";
|
||||
|
||||
/**
|
||||
* 提示词模板管理 - 父级路由
|
||||
* 仅作为嵌套路由的容器,不包含具体内容
|
||||
*/
|
||||
export const handle = {
|
||||
breadcrumb: "提示词模板管理"
|
||||
}
|
||||
|
||||
export default function Prompts() {
|
||||
return <Outlet />;
|
||||
}
|
||||
@@ -1,264 +0,0 @@
|
||||
import { json, redirect, type ActionFunctionArgs, type LoaderFunctionArgs, type MetaFunction } from "@remix-run/node";
|
||||
import { Form, useLoaderData, useNavigation, useActionData } from "@remix-run/react";
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
export const meta: MetaFunction = ({ data }) => {
|
||||
return [
|
||||
{ title: `编辑评查点分组 - ${data?.group?.name || '加载中'} - 中国烟草AI合同及卷宗审核系统` },
|
||||
{ name: "description", content: "编辑评查点分组信息,包括名称、编码、描述和状态" },
|
||||
];
|
||||
};
|
||||
|
||||
// 模拟数据
|
||||
const MOCK_GROUPS = [
|
||||
{
|
||||
id: "1",
|
||||
name: "合同条款类",
|
||||
code: "CONTRACT",
|
||||
description: "关于合同条款的评查点分组",
|
||||
status: "active",
|
||||
sortOrder: 1,
|
||||
createdAt: "2024-01-01T00:00:00Z",
|
||||
updatedAt: "2024-01-01T00:00:00Z",
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
name: "合规性类",
|
||||
code: "COMPLIANCE",
|
||||
description: "关于合规性的评查点分组",
|
||||
status: "active",
|
||||
sortOrder: 2,
|
||||
createdAt: "2024-01-01T00:00:00Z",
|
||||
updatedAt: "2024-01-01T00:00:00Z",
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
name: "风险提示类",
|
||||
code: "RISK",
|
||||
description: "关于风险提示的评查点分组",
|
||||
status: "inactive",
|
||||
sortOrder: 3,
|
||||
createdAt: "2024-01-01T00:00:00Z",
|
||||
updatedAt: "2024-01-01T00:00:00Z",
|
||||
},
|
||||
];
|
||||
|
||||
export async function loader({ params }: LoaderFunctionArgs) {
|
||||
const { groupId } = params;
|
||||
|
||||
// 真实环境中,这里会调用API获取数据
|
||||
// const response = await fetch(`${process.env.API_URL}/api/rule-groups/${groupId}`);
|
||||
// if (response.status === 404) {
|
||||
// throw new Response("评查点分组不存在", { status: 404 });
|
||||
// }
|
||||
// if (!response.ok) {
|
||||
// throw new Response("获取评查点分组失败", { status: response.status });
|
||||
// }
|
||||
// const group = await response.json();
|
||||
|
||||
// 使用模拟数据
|
||||
const group = MOCK_GROUPS.find(g => g.id === groupId);
|
||||
if (!group) {
|
||||
throw new Response("评查点分组不存在", { status: 404 });
|
||||
}
|
||||
|
||||
return json({ group });
|
||||
}
|
||||
|
||||
export async function action({ request, params }: ActionFunctionArgs) {
|
||||
const formData = await request.formData();
|
||||
const groupId = params.groupId;
|
||||
|
||||
const name = formData.get("name");
|
||||
const code = formData.get("code");
|
||||
const description = formData.get("description");
|
||||
const status = formData.get("status");
|
||||
const sortOrder = formData.get("sortOrder");
|
||||
|
||||
// 基本验证
|
||||
const errors = {};
|
||||
if (!name) errors.name = "分组名称不能为空";
|
||||
if (!code) errors.code = "分组编码不能为空";
|
||||
if (Object.keys(errors).length > 0) {
|
||||
return json({ errors, values: Object.fromEntries(formData) });
|
||||
}
|
||||
|
||||
// 构建更新数据
|
||||
const updateData = {
|
||||
name,
|
||||
code,
|
||||
description,
|
||||
status,
|
||||
sortOrder: Number(sortOrder) || 0,
|
||||
};
|
||||
|
||||
// 真实环境中,这里会调用API更新数据
|
||||
// const response = await fetch(`${process.env.API_URL}/api/rule-groups/${groupId}`, {
|
||||
// method: "PUT",
|
||||
// headers: { "Content-Type": "application/json" },
|
||||
// body: JSON.stringify(updateData),
|
||||
// });
|
||||
//
|
||||
// if (!response.ok) {
|
||||
// throw new Response("更新评查点分组失败", { status: response.status });
|
||||
// }
|
||||
|
||||
// 模拟更新成功
|
||||
console.log('保存分组数据:', { id: groupId, ...updateData });
|
||||
|
||||
// 重定向回列表页
|
||||
return redirect('/rule-groups');
|
||||
}
|
||||
|
||||
export default function EditRuleGroup() {
|
||||
const { group } = useLoaderData<typeof loader>();
|
||||
const actionData = useActionData<typeof action>();
|
||||
const navigation = useNavigation();
|
||||
|
||||
const isSubmitting = navigation.state === "submitting";
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
name: group.name,
|
||||
code: group.code,
|
||||
description: group.description || "",
|
||||
status: group.status,
|
||||
sortOrder: group.sortOrder.toString(),
|
||||
});
|
||||
|
||||
// 当actionData中有错误时,保留用户输入的值
|
||||
useEffect(() => {
|
||||
if (actionData?.values) {
|
||||
setFormData({
|
||||
name: actionData.values.name,
|
||||
code: actionData.values.code,
|
||||
description: actionData.values.description || "",
|
||||
status: actionData.values.status,
|
||||
sortOrder: actionData.values.sortOrder,
|
||||
});
|
||||
}
|
||||
}, [actionData]);
|
||||
|
||||
const handleChange = (e) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData(prev => ({ ...prev, [name]: value }));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="edit-rule-group">
|
||||
<div className="mb-6">
|
||||
<h1 className="text-2xl font-medium">编辑评查点分组</h1>
|
||||
</div>
|
||||
|
||||
<div className="card">
|
||||
<Form method="post" className="space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="form-group">
|
||||
<label htmlFor="name" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
分组名称 <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="name"
|
||||
name="name"
|
||||
value={formData.name}
|
||||
onChange={handleChange}
|
||||
className={`w-full h-10 px-3 border ${actionData?.errors?.name ? 'border-red-500' : 'border-gray-300'} rounded-md focus:border-primary focus:ring-1 focus:ring-primary`}
|
||||
placeholder="请输入分组名称"
|
||||
/>
|
||||
{actionData?.errors?.name && (
|
||||
<p className="mt-1 text-sm text-red-500">{actionData.errors.name}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="code" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
分组编码 <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="code"
|
||||
name="code"
|
||||
value={formData.code}
|
||||
onChange={handleChange}
|
||||
className={`w-full h-10 px-3 border ${actionData?.errors?.code ? 'border-red-500' : 'border-gray-300'} rounded-md focus:border-primary focus:ring-1 focus:ring-primary`}
|
||||
placeholder="请输入分组编码"
|
||||
/>
|
||||
{actionData?.errors?.code && (
|
||||
<p className="mt-1 text-sm text-red-500">{actionData.errors.code}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="description" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
分组描述
|
||||
</label>
|
||||
<textarea
|
||||
id="description"
|
||||
name="description"
|
||||
value={formData.description}
|
||||
onChange={handleChange}
|
||||
rows={3}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:border-primary focus:ring-1 focus:ring-primary"
|
||||
placeholder="请输入分组描述"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="form-group">
|
||||
<label htmlFor="status" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
状态
|
||||
</label>
|
||||
<select
|
||||
id="status"
|
||||
name="status"
|
||||
value={formData.status}
|
||||
onChange={handleChange}
|
||||
className="w-full h-10 px-3 border border-gray-300 rounded-md focus:border-primary focus:ring-1 focus:ring-primary"
|
||||
>
|
||||
<option value="active">启用</option>
|
||||
<option value="inactive">禁用</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="sortOrder" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
排序
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
id="sortOrder"
|
||||
name="sortOrder"
|
||||
value={formData.sortOrder}
|
||||
onChange={handleChange}
|
||||
className="w-full h-10 px-3 border border-gray-300 rounded-md focus:border-primary focus:ring-1 focus:ring-primary"
|
||||
placeholder="请输入排序值"
|
||||
min="0"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end space-x-3 pt-4 border-t border-gray-100">
|
||||
<a
|
||||
href="/rule-groups"
|
||||
className="ant-btn ant-btn-default"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
window.history.back();
|
||||
}}
|
||||
>
|
||||
取消
|
||||
</a>
|
||||
<button
|
||||
type="submit"
|
||||
className="ant-btn ant-btn-primary"
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{isSubmitting ? '保存中...' : '保存'}
|
||||
</button>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -9,6 +9,10 @@ import { Table } from "~/components/ui/Table";
|
||||
import { FilterPanel, FilterSelect, SearchFilter } from "~/components/ui/FilterPanel";
|
||||
import { Pagination } from "~/components/ui/Pagination";
|
||||
|
||||
export function links() {
|
||||
return [{ rel: "stylesheet", href: indexStyles }];
|
||||
}
|
||||
|
||||
// 定义数据类型
|
||||
interface RuleGroup {
|
||||
id: string;
|
||||
@@ -21,10 +25,6 @@ interface RuleGroup {
|
||||
children?: RuleGroup[];
|
||||
}
|
||||
|
||||
export const handle = {
|
||||
breadcrumb: "评查点分组"
|
||||
};
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return [
|
||||
{ title: "评查点分组 - 中国烟草AI合同及卷宗审核系统" },
|
||||
@@ -32,9 +32,6 @@ export const meta: MetaFunction = () => {
|
||||
];
|
||||
};
|
||||
|
||||
export function links() {
|
||||
return [{ rel: "stylesheet", href: indexStyles }];
|
||||
}
|
||||
|
||||
// 模拟数据
|
||||
const MOCK_GROUPS: RuleGroup[] = [
|
||||
@@ -263,20 +260,21 @@ export default function RuleGroupsIndex() {
|
||||
key: "operation",
|
||||
width: "180px",
|
||||
render: (_: unknown, record: RuleGroup) => (
|
||||
<>
|
||||
<button
|
||||
className="ant-btn ant-btn-text ant-btn-sm text-primary"
|
||||
onClick={() => navigate(`/rule-groups/${record.id}`)}
|
||||
<div className="operations-cell">
|
||||
<button
|
||||
onClick={() => navigate(`/rule-groups/new?id=${record.id}`)}
|
||||
className="operation-btn"
|
||||
>
|
||||
<i className="ri-edit-line"></i> 编辑
|
||||
</button>
|
||||
<button
|
||||
className="ant-btn ant-btn-text ant-btn-sm text-error"
|
||||
type="button"
|
||||
className="operation-btn !text-[--color-error]"
|
||||
onClick={() => handleDeleteGroup(record.id)}
|
||||
>
|
||||
<i className="ri-delete-bin-line"></i> 删除
|
||||
</button>
|
||||
</>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
+458
-166
@@ -1,205 +1,497 @@
|
||||
// app/routes/rule-groups.new.tsx
|
||||
import { json, redirect, type ActionFunctionArgs, type MetaFunction } from "@remix-run/node";
|
||||
import { Form, useNavigation, useActionData } from "@remix-run/react";
|
||||
import { useState, useEffect } from "react";
|
||||
import { redirect, json, type ActionFunctionArgs, type LoaderFunctionArgs, type MetaFunction } from "@remix-run/node";
|
||||
import { useLoaderData, useActionData, useNavigation, Form } from "@remix-run/react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Button } from "~/components/ui/Button";
|
||||
import { Card } from "~/components/ui/Card";
|
||||
import ruleGroupsNewStyles from "~/styles/pages/rule-groups_new.css?url";
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
// 类型定义
|
||||
interface RuleGroup {
|
||||
id: string;
|
||||
name: string;
|
||||
code: string;
|
||||
description?: string;
|
||||
status: 'active' | 'inactive';
|
||||
parentId?: string | null;
|
||||
sortOrder?: number;
|
||||
}
|
||||
|
||||
interface ParentGroup {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
// 定义加载器返回数据类型
|
||||
export interface LoaderData {
|
||||
group?: RuleGroup;
|
||||
parentGroups: ParentGroup[];
|
||||
isEdit: boolean;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
// 定义action返回数据类型
|
||||
export interface ActionData {
|
||||
success?: boolean;
|
||||
errors?: {
|
||||
name?: string;
|
||||
code?: string;
|
||||
parentId?: string;
|
||||
general?: string;
|
||||
};
|
||||
values?: Record<string, string>;
|
||||
}
|
||||
|
||||
// 样式链接
|
||||
export function links() {
|
||||
return [{ rel: "stylesheet", href: ruleGroupsNewStyles }];
|
||||
}
|
||||
|
||||
// 动态面包屑
|
||||
export const handle = {
|
||||
breadcrumb: (data: LoaderData) => {
|
||||
return data.isEdit ? "编辑分组" : "新增分组";
|
||||
}
|
||||
};
|
||||
|
||||
// 页面元数据
|
||||
export const meta: MetaFunction = ({ location }) => {
|
||||
const isEdit = new URLSearchParams(location.search).has("id");
|
||||
const title = isEdit ? "编辑评查点分组" : "新建评查点分组";
|
||||
|
||||
return [
|
||||
{ title: "新建评查点分组 - 中国烟草AI合同及卷宗审核系统" },
|
||||
{ title: `${title} - 中国烟草AI合同及卷宗审核系统` },
|
||||
{ name: "description", content: "创建新的评查点分组,包括分组名称、编码、描述和状态" },
|
||||
];
|
||||
};
|
||||
|
||||
// 数据加载器
|
||||
export async function loader({ request }: LoaderFunctionArgs) {
|
||||
console.log("rule-groups.new loader被调用,URL:", request.url);
|
||||
try {
|
||||
const url = new URL(request.url);
|
||||
const id = url.searchParams.get("id");
|
||||
console.log("获取到的ID参数:", id);
|
||||
|
||||
// 获取一级分组列表 (用于选择父级分组)
|
||||
const parentGroups: ParentGroup[] = [
|
||||
{ id: "1", name: "合同基本要素检查" },
|
||||
{ id: "4", name: "销售合同专项检查" },
|
||||
{ id: "5", name: "行政处罚规范性检查" }
|
||||
];
|
||||
|
||||
// 简化这里,初始时不获取数据,避免可能的错误
|
||||
let group: RuleGroup | undefined = undefined;
|
||||
|
||||
// 如果有ID才尝试获取数据
|
||||
if (id) {
|
||||
// 简化的模拟数据
|
||||
const demoData: Record<string, RuleGroup> = {
|
||||
"1": {
|
||||
id: "1",
|
||||
name: "合同基本要素检查",
|
||||
code: "contract-base",
|
||||
description: "检查合同基本要素是否完整规范",
|
||||
status: "active",
|
||||
parentId: null,
|
||||
sortOrder: 0
|
||||
},
|
||||
"2": {
|
||||
id: "2",
|
||||
name: "必备要素检查",
|
||||
code: "essential-elements",
|
||||
description: "检查合同中的必要信息项是否齐全",
|
||||
status: "active",
|
||||
parentId: "1",
|
||||
sortOrder: 1
|
||||
},
|
||||
"3": {
|
||||
id: "3",
|
||||
name: "合同主体检查",
|
||||
code: "contract-parties",
|
||||
description: "检查合同主体信息是否规范",
|
||||
status: "active",
|
||||
parentId: "1",
|
||||
sortOrder: 2
|
||||
},
|
||||
"4": {
|
||||
id: "4",
|
||||
name: "销售合同专项检查",
|
||||
code: "contract-sales",
|
||||
description: "针对销售合同的专项合规检查",
|
||||
status: "active",
|
||||
parentId: null,
|
||||
sortOrder: 3
|
||||
},
|
||||
"5": {
|
||||
id: "5",
|
||||
name: "行政处罚规范性检查",
|
||||
code: "punishment",
|
||||
description: "对行政处罚文书的合规性检查",
|
||||
status: "inactive",
|
||||
parentId: null,
|
||||
sortOrder: 4
|
||||
}
|
||||
};
|
||||
|
||||
group = demoData[id];
|
||||
console.log("找到的group数据:", group);
|
||||
}
|
||||
|
||||
// 返回简化的数据结构
|
||||
return json<LoaderData>({
|
||||
group,
|
||||
parentGroups,
|
||||
isEdit: !!group,
|
||||
error: undefined // 显式返回error字段,无错误时为undefined
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("loader函数出错:", error);
|
||||
// 返回一个基本的响应,避免500错误
|
||||
return json<LoaderData>({
|
||||
group: undefined,
|
||||
parentGroups: [],
|
||||
isEdit: false,
|
||||
error: "加载数据时出错"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 表单处理
|
||||
export async function action({ request }: ActionFunctionArgs) {
|
||||
const formData = await request.formData();
|
||||
|
||||
const name = formData.get("name");
|
||||
const code = formData.get("code");
|
||||
const description = formData.get("description");
|
||||
const status = formData.get("status") || "active";
|
||||
const sortOrder = formData.get("sortOrder") || "0";
|
||||
// 提取表单数据
|
||||
const id = formData.get("id") as string | null;
|
||||
const name = formData.get("name") as string;
|
||||
const code = formData.get("code") as string;
|
||||
const description = formData.get("description") as string;
|
||||
const status = formData.get("status") as string || "active";
|
||||
const groupType = formData.get("groupType") as string;
|
||||
const parentId = groupType === "secondary" ? formData.get("parentId") as string : null;
|
||||
const sortOrder = parseInt(formData.get("sortOrder") as string || "0", 10);
|
||||
|
||||
// 基本验证
|
||||
const errors = {};
|
||||
if (!name) errors.name = "分组名称不能为空";
|
||||
if (!code) errors.code = "分组编码不能为空";
|
||||
if (Object.keys(errors).length > 0) {
|
||||
return json({ errors, values: Object.fromEntries(formData) });
|
||||
// 表单验证
|
||||
const errors: ActionData["errors"] = {};
|
||||
|
||||
if (!name || name.trim() === "") {
|
||||
errors.name = "分组名称不能为空";
|
||||
}
|
||||
|
||||
// 构建创建数据
|
||||
const createData = {
|
||||
name,
|
||||
code,
|
||||
description,
|
||||
if (!code || code.trim() === "") {
|
||||
errors.code = "分组编码不能为空";
|
||||
} else if (!/^[a-zA-Z0-9-]+$/.test(code)) {
|
||||
errors.code = "分组编码只能包含字母、数字和连字符";
|
||||
}
|
||||
|
||||
if (groupType === "secondary" && (!parentId || parentId.trim() === "")) {
|
||||
errors.parentId = "请选择上级分组";
|
||||
}
|
||||
|
||||
if (Object.keys(errors).length > 0) {
|
||||
return json<ActionData>({
|
||||
errors,
|
||||
values: Object.fromEntries(formData) as Record<string, string>
|
||||
});
|
||||
}
|
||||
|
||||
// 构建保存数据
|
||||
const saveData = {
|
||||
id,
|
||||
name: name.trim(),
|
||||
code: code.trim(),
|
||||
description: description?.trim() || "",
|
||||
status,
|
||||
sortOrder: Number(sortOrder) || 0,
|
||||
parentId,
|
||||
sortOrder
|
||||
};
|
||||
|
||||
// 真实环境中,这里会调用API创建数据
|
||||
// const response = await fetch(`${process.env.API_URL}/api/rule-groups`, {
|
||||
// method: "POST",
|
||||
// headers: { "Content-Type": "application/json" },
|
||||
// body: JSON.stringify(createData),
|
||||
// });
|
||||
//
|
||||
// if (!response.ok) {
|
||||
// throw new Response("创建评查点分组失败", { status: response.status });
|
||||
// }
|
||||
|
||||
// 模拟创建成功
|
||||
console.log('创建分组数据:', createData);
|
||||
|
||||
// 重定向回列表页
|
||||
return redirect('/rule-groups');
|
||||
try {
|
||||
// 实际应用中应调用API
|
||||
console.log("保存分组数据:", saveData);
|
||||
|
||||
// const response = await fetch(`${process.env.API_BASE_URL}/api/rule-groups${id ? `/${id}` : ''}`, {
|
||||
// method: id ? "PUT" : "POST",
|
||||
// headers: {
|
||||
// "Content-Type": "application/json",
|
||||
// },
|
||||
// body: JSON.stringify(saveData),
|
||||
// });
|
||||
//
|
||||
// if (!response.ok) {
|
||||
// throw new Error(`保存失败: ${response.status}`);
|
||||
// }
|
||||
|
||||
// 保存成功,重定向到列表页
|
||||
return redirect("/rule-groups");
|
||||
} catch (error) {
|
||||
console.error("保存分组失败:", error);
|
||||
return json<ActionData>({
|
||||
success: false,
|
||||
errors: {
|
||||
general: "保存分组失败,请稍后重试"
|
||||
},
|
||||
values: Object.fromEntries(formData) as Record<string, string>
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default function NewRuleGroup() {
|
||||
// 页面组件
|
||||
export default function RuleGroupNew() {
|
||||
// 所有Hooks必须在组件顶部无条件调用
|
||||
const data = useLoaderData<typeof loader>();
|
||||
const actionData = useActionData<typeof action>();
|
||||
const navigation = useNavigation();
|
||||
|
||||
const isSubmitting = navigation.state === "submitting";
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
name: "",
|
||||
code: "",
|
||||
description: "",
|
||||
status: "active",
|
||||
sortOrder: "0",
|
||||
});
|
||||
// 表单状态
|
||||
const [groupType, setGroupType] = useState<"primary" | "secondary">("primary");
|
||||
const [showParentSelect, setShowParentSelect] = useState(false);
|
||||
|
||||
// 当actionData中有错误时,保留用户输入的值
|
||||
// 解构数据
|
||||
const { group, parentGroups, isEdit, error } = data;
|
||||
|
||||
// 初始化表单状态
|
||||
useEffect(() => {
|
||||
if (actionData?.values) {
|
||||
setFormData({
|
||||
name: actionData.values.name,
|
||||
code: actionData.values.code,
|
||||
description: actionData.values.description || "",
|
||||
status: actionData.values.status,
|
||||
sortOrder: actionData.values.sortOrder,
|
||||
});
|
||||
if (group) {
|
||||
if (group.parentId) {
|
||||
setGroupType("secondary");
|
||||
setShowParentSelect(true);
|
||||
} else {
|
||||
setGroupType("primary");
|
||||
setShowParentSelect(false);
|
||||
}
|
||||
}
|
||||
}, [actionData]);
|
||||
}, [group]);
|
||||
|
||||
const handleChange = (e) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData(prev => ({ ...prev, [name]: value }));
|
||||
// 处理分组类型变更
|
||||
const handleGroupTypeChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = e.target.value as "primary" | "secondary";
|
||||
setGroupType(value);
|
||||
setShowParentSelect(value === "secondary");
|
||||
};
|
||||
|
||||
// 如果加载数据时出错,显示错误信息
|
||||
if (error) {
|
||||
return (
|
||||
<div className="error-container">
|
||||
<h1>加载出错</h1>
|
||||
<p>{error}</p>
|
||||
<Button type="default" to="/rule-groups">
|
||||
<i className="ri-arrow-left-line"></i> 返回列表
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="new-rule-group">
|
||||
<div className="mb-6">
|
||||
<h1 className="text-2xl font-medium">新建评查点分组</h1>
|
||||
<div className="rule-group-new-page">
|
||||
{/* 页面头部 */}
|
||||
<div className="page-header">
|
||||
<div>
|
||||
<h1 className="page-title">{isEdit ? "编辑评查点分组" : "新增评查点分组"}</h1>
|
||||
<p className="page-subtitle">创建新的评查点分组,用于组织管理评查点</p>
|
||||
</div>
|
||||
<div className="header-actions">
|
||||
<Button
|
||||
type="default"
|
||||
to="/rule-groups"
|
||||
className="mr-3"
|
||||
>
|
||||
<i className="ri-arrow-left-line"></i> 返回列表
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
form="group-form"
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
<i className="ri-save-line"></i> {isSubmitting ? '保存中...' : '保存分组'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card">
|
||||
<Form method="post" className="space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="form-group">
|
||||
<label htmlFor="name" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
分组名称 <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="name"
|
||||
name="name"
|
||||
value={formData.name}
|
||||
onChange={handleChange}
|
||||
className={`w-full h-10 px-3 border ${actionData?.errors?.name ? 'border-red-500' : 'border-gray-300'} rounded-md focus:border-primary focus:ring-1 focus:ring-primary`}
|
||||
placeholder="请输入分组名称"
|
||||
/>
|
||||
{actionData?.errors?.name && (
|
||||
<p className="mt-1 text-sm text-red-500">{actionData.errors.name}</p>
|
||||
<div className="form-container">
|
||||
{/* 提示信息 */}
|
||||
<div className="info-message">
|
||||
<i className="ri-information-line"></i>
|
||||
<p>评查点分组用于对评查点进行分类管理,合理的分组结构有助于更高效地组织和查找评查点。</p>
|
||||
</div>
|
||||
|
||||
{/* 错误提示 */}
|
||||
{actionData?.errors?.general && (
|
||||
<div className="general-error">
|
||||
<i className="ri-error-warning-line mr-2"></i>
|
||||
{actionData.errors.general}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 表单 */}
|
||||
<Form method="post" id="group-form">
|
||||
{/* 如果是编辑模式,添加ID */}
|
||||
{group?.id && <input type="hidden" name="id" value={group.id} />}
|
||||
|
||||
{/* 基本信息区域 */}
|
||||
<Card className="form-section">
|
||||
<div className="form-section-header">
|
||||
<i className="ri-file-info-line"></i>
|
||||
<h3>基本信息</h3>
|
||||
</div>
|
||||
<div className="form-section-body">
|
||||
{/* 分组类型选择 */}
|
||||
<div className="form-group">
|
||||
<legend className="form-label" id="groupTypeLabel">
|
||||
分组类型 <span className="required-mark">*</span>
|
||||
</legend>
|
||||
<div className="radio-group" role="radiogroup" aria-labelledby="groupTypeLabel">
|
||||
<label className="radio-item" htmlFor="groupType-primary">
|
||||
<input
|
||||
type="radio"
|
||||
id="groupType-primary"
|
||||
name="groupType"
|
||||
className="radio-input"
|
||||
value="primary"
|
||||
checked={groupType === "primary"}
|
||||
onChange={handleGroupTypeChange}
|
||||
/>
|
||||
<span>一级分组</span>
|
||||
</label>
|
||||
<label className="radio-item" htmlFor="groupType-secondary">
|
||||
<input
|
||||
type="radio"
|
||||
id="groupType-secondary"
|
||||
name="groupType"
|
||||
className="radio-input"
|
||||
value="secondary"
|
||||
checked={groupType === "secondary"}
|
||||
onChange={handleGroupTypeChange}
|
||||
/>
|
||||
<span>二级分组</span>
|
||||
</label>
|
||||
</div>
|
||||
<div className="form-tip">一级分组作为顶层分类,二级分组需要选择所属的一级分组</div>
|
||||
</div>
|
||||
|
||||
{/* 上级分组选择 */}
|
||||
{showParentSelect && (
|
||||
<div className="form-group">
|
||||
<label htmlFor="parentId" className="form-label">
|
||||
上级分组 <span className="required-mark">*</span>
|
||||
</label>
|
||||
<select
|
||||
id="parentId"
|
||||
name="parentId"
|
||||
className={`form-select ${actionData?.errors?.parentId ? 'error' : ''}`}
|
||||
defaultValue={group?.parentId || ""}
|
||||
>
|
||||
<option value="">请选择上级分组</option>
|
||||
{parentGroups.map((parent) => (
|
||||
<option key={parent.id} value={parent.id}>
|
||||
{parent.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{actionData?.errors?.parentId && (
|
||||
<div className="form-error">{actionData.errors.parentId}</div>
|
||||
)}
|
||||
<div className="form-tip">选择此分组所属的上级分组</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 分组编码和名称 */}
|
||||
<div className="form-row">
|
||||
<div className="form-col">
|
||||
<div className="form-group">
|
||||
<label htmlFor="code" className="form-label">
|
||||
分组编码 <span className="required-mark">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="code"
|
||||
name="code"
|
||||
className={`form-input ${actionData?.errors?.code ? 'error' : ''}`}
|
||||
defaultValue={group?.code || actionData?.values?.code || ""}
|
||||
placeholder="请输入分组编码,如contract-base"
|
||||
/>
|
||||
{actionData?.errors?.code && (
|
||||
<div className="form-error">{actionData.errors.code}</div>
|
||||
)}
|
||||
<div className="form-tip">编码只能包含字母、数字和连字符,且必须唯一</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-col">
|
||||
<div className="form-group">
|
||||
<label htmlFor="name" className="form-label">
|
||||
分组名称 <span className="required-mark">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="name"
|
||||
name="name"
|
||||
className={`form-input ${actionData?.errors?.name ? 'error' : ''}`}
|
||||
defaultValue={group?.name || actionData?.values?.name || ""}
|
||||
placeholder="请输入分组名称,如合同基本要素检查"
|
||||
/>
|
||||
{actionData?.errors?.name && (
|
||||
<div className="form-error">{actionData.errors.name}</div>
|
||||
)}
|
||||
<div className="form-tip">请使用简洁明了的名称,不超过30个字符</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="code" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
分组编码 <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="code"
|
||||
name="code"
|
||||
value={formData.code}
|
||||
onChange={handleChange}
|
||||
className={`w-full h-10 px-3 border ${actionData?.errors?.code ? 'border-red-500' : 'border-gray-300'} rounded-md focus:border-primary focus:ring-1 focus:ring-primary`}
|
||||
placeholder="请输入分组编码"
|
||||
/>
|
||||
{actionData?.errors?.code && (
|
||||
<p className="mt-1 text-sm text-red-500">{actionData.errors.code}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="description" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
分组描述
|
||||
</label>
|
||||
<textarea
|
||||
id="description"
|
||||
name="description"
|
||||
value={formData.description}
|
||||
onChange={handleChange}
|
||||
rows={3}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:border-primary focus:ring-1 focus:ring-primary"
|
||||
placeholder="请输入分组描述"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="form-group">
|
||||
<label htmlFor="status" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
状态
|
||||
</label>
|
||||
<select
|
||||
id="status"
|
||||
name="status"
|
||||
value={formData.status}
|
||||
onChange={handleChange}
|
||||
className="w-full h-10 px-3 border border-gray-300 rounded-md focus:border-primary focus:ring-1 focus:ring-primary"
|
||||
>
|
||||
<option value="active">启用</option>
|
||||
<option value="inactive">禁用</option>
|
||||
</select>
|
||||
{/* 详细配置区域 */}
|
||||
<Card className="form-section">
|
||||
<div className="form-section-header">
|
||||
<i className="ri-settings-4-line"></i>
|
||||
<h3>详细配置</h3>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="sortOrder" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
排序
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
id="sortOrder"
|
||||
name="sortOrder"
|
||||
value={formData.sortOrder}
|
||||
onChange={handleChange}
|
||||
className="w-full h-10 px-3 border border-gray-300 rounded-md focus:border-primary focus:ring-1 focus:ring-primary"
|
||||
placeholder="请输入排序值"
|
||||
min="0"
|
||||
/>
|
||||
<div className="form-section-body">
|
||||
{/* 分组描述 */}
|
||||
<div className="form-group">
|
||||
<label htmlFor="description" className="form-label">分组描述</label>
|
||||
<textarea
|
||||
id="description"
|
||||
name="description"
|
||||
className="form-textarea"
|
||||
defaultValue={group?.description || actionData?.values?.description || ""}
|
||||
placeholder="请输入分组描述,包括适用场景、分组目的等"
|
||||
></textarea>
|
||||
<div className="form-tip">详细描述有助于其他用户了解该分组的用途</div>
|
||||
</div>
|
||||
|
||||
{/* 状态 */}
|
||||
<div className="form-group" style={{ maxWidth: "400px" }}>
|
||||
<label htmlFor="status" className="form-label">状态</label>
|
||||
<select
|
||||
id="status"
|
||||
name="status"
|
||||
className="form-select"
|
||||
defaultValue={group?.status || actionData?.values?.status || "active"}
|
||||
>
|
||||
<option value="active">启用</option>
|
||||
<option value="inactive">禁用</option>
|
||||
</select>
|
||||
<div className="form-tip">禁用状态的分组及其下的评查点将不会参与评查</div>
|
||||
</div>
|
||||
|
||||
{/* 排序 */}
|
||||
<div className="form-group" style={{ maxWidth: "400px" }}>
|
||||
<label htmlFor="sortOrder" className="form-label">排序</label>
|
||||
<input
|
||||
type="number"
|
||||
id="sortOrder"
|
||||
name="sortOrder"
|
||||
className="form-input"
|
||||
defaultValue={group?.sortOrder?.toString() || actionData?.values?.sortOrder || "0"}
|
||||
placeholder="请输入排序值,数字越小排序越靠前"
|
||||
min="0"
|
||||
/>
|
||||
<div className="form-tip">用于设置分组在列表中的显示顺序,默认为0</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end space-x-3 pt-4 border-t border-gray-100">
|
||||
<a
|
||||
href="/rule-groups"
|
||||
className="ant-btn ant-btn-default"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
window.history.back();
|
||||
}}
|
||||
>
|
||||
取消
|
||||
</a>
|
||||
<button
|
||||
type="submit"
|
||||
className="ant-btn ant-btn-primary"
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{isSubmitting ? '创建中...' : '创建'}
|
||||
</button>
|
||||
</div>
|
||||
</Card>
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
// app/routes/rule-groups.tsx
|
||||
import { Outlet } from "@remix-run/react";
|
||||
|
||||
/**
|
||||
* 评查点分组管理 - 父级路由
|
||||
* 仅作为嵌套路由的容器,不包含具体内容
|
||||
*/
|
||||
export const handle = {
|
||||
breadcrumb: "评查规则库"
|
||||
};
|
||||
|
||||
export default function RuleGroupsLayout() {
|
||||
return <Outlet />
|
||||
breadcrumb: "评查点分组"
|
||||
}
|
||||
export default function RuleGroups() {
|
||||
return <Outlet />;
|
||||
}
|
||||
@@ -94,7 +94,7 @@
|
||||
}
|
||||
|
||||
.ant-btn-default {
|
||||
@apply bg-white border border-gray-300 text-gray-800 hover:border-[#00684a] focus:ring-[#00684a];
|
||||
@apply bg-white border border-gray-300 text-gray-800 focus:ring-gray-300;
|
||||
}
|
||||
|
||||
.ant-btn-danger {
|
||||
|
||||
@@ -24,10 +24,10 @@
|
||||
|
||||
/* 筛选控件 */
|
||||
.filter-control {
|
||||
@apply w-full;
|
||||
@apply w-full focus:border-[#00684a] focus:shadow-[0,0,0,2px,rgba(0,104,74,0.2)] focus:outline-none;
|
||||
}
|
||||
|
||||
/* 筛选操作按钮区域 */
|
||||
/* 筛选操作按钮区域 */
|
||||
.filter-actions {
|
||||
@apply flex justify-end items-center pt-4 mt-4 border-t border-gray-100 space-x-3;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
/**
|
||||
* 提示词模板管理页面样式
|
||||
*/
|
||||
|
||||
.prompt-page {
|
||||
--primary-color: var(--color-primary, #00684a);
|
||||
--primary-hover: var(--color-primary-hover, #005a40);
|
||||
--primary-light: rgba(0, 104, 74, 0.1);
|
||||
--success-color: var(--color-success, #52c41a);
|
||||
--warning-color: var(--color-warning, #faad14);
|
||||
--error-color: var(--color-error, #ff4d4f);
|
||||
}
|
||||
|
||||
/* 页面头部 */
|
||||
.prompt-page .page-header {
|
||||
@apply flex justify-between items-center mb-4;
|
||||
}
|
||||
|
||||
.prompt-page .page-title {
|
||||
@apply text-xl font-medium;
|
||||
}
|
||||
|
||||
/* 搜索区域 */
|
||||
.prompt-page .search-container {
|
||||
@apply mb-4;
|
||||
}
|
||||
|
||||
.prompt-page .search-form {
|
||||
@apply flex flex-wrap items-end gap-4;
|
||||
}
|
||||
|
||||
.prompt-page .search-field {
|
||||
@apply flex-1 min-w-[200px];
|
||||
}
|
||||
|
||||
.prompt-page .search-actions {
|
||||
@apply flex items-center;
|
||||
}
|
||||
|
||||
/* 数据表格 */
|
||||
.prompt-page .table-container {
|
||||
@apply overflow-x-auto;
|
||||
}
|
||||
|
||||
/* 类型标签 */
|
||||
.prompt-page .type-badge {
|
||||
@apply inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium mr-1;
|
||||
}
|
||||
|
||||
.prompt-page .type-extraction {
|
||||
@apply bg-green-100 text-green-800;
|
||||
}
|
||||
|
||||
.prompt-page .type-evaluation {
|
||||
@apply bg-yellow-100 text-yellow-800;
|
||||
}
|
||||
|
||||
.prompt-page .type-summary {
|
||||
@apply bg-blue-100 text-blue-800;
|
||||
}
|
||||
|
||||
.prompt-page .type-common {
|
||||
@apply bg-purple-100 text-purple-800;
|
||||
}
|
||||
|
||||
/* 状态标签 */
|
||||
.prompt-page .status-badge {
|
||||
@apply inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium;
|
||||
}
|
||||
|
||||
.prompt-page .status-active {
|
||||
@apply bg-green-100 text-green-800;
|
||||
}
|
||||
|
||||
.prompt-page .status-inactive {
|
||||
@apply bg-red-100 text-red-800;
|
||||
}
|
||||
|
||||
.prompt-page .status-system {
|
||||
@apply bg-blue-100 text-blue-800;
|
||||
}
|
||||
|
||||
/* 操作按钮 */
|
||||
.prompt-page .operation-btn {
|
||||
@apply inline-flex items-center px-2 py-1 text-sm rounded-md hover:bg-gray-100 transition-colors duration-150 ease-in-out;
|
||||
}
|
||||
|
||||
.prompt-page .operation-btn i {
|
||||
@apply mr-1;
|
||||
}
|
||||
|
||||
/* 分页 */
|
||||
.prompt-page .pagination-info {
|
||||
@apply text-sm text-gray-500;
|
||||
}
|
||||
|
||||
.prompt-page .pagination-controls {
|
||||
@apply flex items-center;
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
/**
|
||||
* 提示词模板编辑页面样式
|
||||
*/
|
||||
|
||||
.prompt-new-page {
|
||||
--primary-color: var(--color-primary, #00684a);
|
||||
--primary-hover: var(--color-primary-hover, #005a40);
|
||||
--primary-light: rgba(0, 104, 74, 0.1);
|
||||
--success-color: var(--color-success, #52c41a);
|
||||
--warning-color: var(--color-warning, #faad14);
|
||||
--error-color: var(--color-error, #ff4d4f);
|
||||
--text-color: rgba(0, 0, 0, 0.85);
|
||||
--text-secondary: rgba(0, 0, 0, 0.45);
|
||||
--border-color: #f0f0f0;
|
||||
--bg-gray: #f5f5f5;
|
||||
}
|
||||
|
||||
/* 页面头部 */
|
||||
.prompt-new-page .page-header {
|
||||
@apply flex justify-between items-center mb-4;
|
||||
}
|
||||
|
||||
.prompt-new-page .page-title {
|
||||
@apply text-xl font-medium;
|
||||
}
|
||||
|
||||
/* 表单样式 */
|
||||
.prompt-new-page .form-group {
|
||||
@apply mb-5;
|
||||
}
|
||||
|
||||
.prompt-new-page .form-label {
|
||||
@apply block mb-1.5 text-sm font-medium text-gray-800;
|
||||
}
|
||||
|
||||
.prompt-new-page .form-input,
|
||||
.prompt-new-page .form-select,
|
||||
.prompt-new-page .form-textarea {
|
||||
@apply w-full px-3 py-2 text-sm border border-gray-300 rounded transition-all duration-300;
|
||||
}
|
||||
|
||||
.prompt-new-page .form-input:focus,
|
||||
.prompt-new-page .form-select:focus,
|
||||
.prompt-new-page .form-textarea:focus {
|
||||
@apply outline-none border-[var(--primary-color)] shadow-[0_0_0_2px_rgba(0,104,74,0.2)];
|
||||
}
|
||||
|
||||
.prompt-new-page .form-textarea {
|
||||
@apply min-h-[80px];
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.prompt-new-page .form-code-editor {
|
||||
@apply font-mono text-sm p-3 min-h-[300px] rounded border border-gray-300 w-full whitespace-pre-wrap;
|
||||
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.prompt-new-page .help-text {
|
||||
@apply text-[var(--text-secondary)] text-xs mt-1;
|
||||
}
|
||||
|
||||
/* 只读字段 */
|
||||
.prompt-new-page .read-only-field {
|
||||
@apply bg-[var(--bg-gray)] cursor-not-allowed;
|
||||
}
|
||||
|
||||
/* 变量相关样式 */
|
||||
.prompt-new-page .var-container {
|
||||
@apply flex flex-wrap gap-2 mt-2;
|
||||
}
|
||||
|
||||
.prompt-new-page .var-badge {
|
||||
@apply inline-flex items-center px-2 py-1 rounded-full bg-[var(--primary-light)] text-[var(--primary-color)] text-xs mr-1.5 mb-1.5;
|
||||
}
|
||||
|
||||
.prompt-new-page .var-badge i {
|
||||
@apply text-sm ml-1 cursor-pointer;
|
||||
}
|
||||
|
||||
.prompt-new-page .example-section {
|
||||
@apply mt-2 border border-[var(--border-color)] rounded p-3;
|
||||
}
|
||||
|
||||
.prompt-new-page .example-header {
|
||||
@apply font-medium mb-2 text-sm;
|
||||
}
|
||||
|
||||
.prompt-new-page .var-input-group {
|
||||
@apply flex gap-2 mb-2;
|
||||
}
|
||||
|
||||
.prompt-new-page .var-input-group .form-input {
|
||||
@apply flex-1 mb-0;
|
||||
}
|
||||
|
||||
/* 开关样式 */
|
||||
.prompt-new-page .switch {
|
||||
@apply relative inline-block w-11 h-[22px];
|
||||
}
|
||||
|
||||
.prompt-new-page .switch input {
|
||||
@apply opacity-0 w-0 h-0;
|
||||
}
|
||||
|
||||
.prompt-new-page .slider {
|
||||
@apply absolute cursor-pointer top-0 left-0 right-0 bottom-0 bg-gray-400 rounded-full transition-all duration-300;
|
||||
}
|
||||
|
||||
.prompt-new-page .slider:before {
|
||||
content: "";
|
||||
@apply absolute h-[18px] w-[18px] left-[3px] bottom-[2px] bg-white rounded-full transition-all duration-300;
|
||||
}
|
||||
|
||||
.prompt-new-page input:checked + .slider {
|
||||
@apply bg-[var(--primary-color)];
|
||||
}
|
||||
|
||||
.prompt-new-page input:focus + .slider {
|
||||
@apply shadow-[0_0_1px_var(--primary-color)];
|
||||
}
|
||||
|
||||
.prompt-new-page input:checked + .slider:before {
|
||||
@apply transform translate-x-5;
|
||||
}
|
||||
|
||||
/* 警告框样式 */
|
||||
.prompt-new-page .alert {
|
||||
@apply p-3 rounded mb-4 flex items-start;
|
||||
}
|
||||
|
||||
.prompt-new-page .alert i {
|
||||
@apply mr-2 mt-0.5;
|
||||
}
|
||||
|
||||
.prompt-new-page .alert-info {
|
||||
@apply bg-[#e6f7ff] border border-[#91d5ff] text-[#1890ff];
|
||||
}
|
||||
|
||||
.prompt-new-page .alert-warning {
|
||||
@apply bg-[#fffbe6] border border-[#ffe58f] text-[#faad14];
|
||||
}
|
||||
|
||||
/* 类型标签 */
|
||||
.prompt-new-page .type-tag {
|
||||
@apply inline-flex items-center px-2 py-1 rounded text-xs mb-2;
|
||||
}
|
||||
|
||||
.prompt-new-page .type-common {
|
||||
@apply bg-[var(--primary-light)] text-[var(--primary-color)];
|
||||
}
|
||||
|
||||
.prompt-new-page .type-extraction {
|
||||
@apply bg-[rgba(82,196,26,0.1)] text-[var(--success-color)];
|
||||
}
|
||||
|
||||
.prompt-new-page .type-evaluation {
|
||||
@apply bg-[rgba(250,173,20,0.1)] text-[var(--warning-color)];
|
||||
}
|
||||
|
||||
.prompt-new-page .type-summary {
|
||||
@apply bg-[rgba(24,144,255,0.1)] text-[#1890ff];
|
||||
}
|
||||
|
||||
/* 代码块样式 */
|
||||
.prompt-new-page code {
|
||||
@apply bg-gray-100 text-gray-800 px-1 py-0.5 rounded text-sm font-mono;
|
||||
}
|
||||
|
||||
/* 文本颜色 */
|
||||
.prompt-new-page .text-error {
|
||||
@apply text-[var(--error-color)];
|
||||
}
|
||||
|
||||
.prompt-new-page .text-secondary {
|
||||
@apply text-[var(--text-secondary)];
|
||||
}
|
||||
|
||||
.prompt-new-page .text-primary {
|
||||
@apply text-[var(--primary-color)];
|
||||
}
|
||||
|
||||
/* 卡片样式 */
|
||||
.prompt-new-page .ant-card {
|
||||
@apply bg-white rounded-lg shadow-sm border border-gray-200 mb-4;
|
||||
}
|
||||
|
||||
.prompt-new-page .ant-card-header {
|
||||
@apply px-4 py-3 border-b border-gray-100 font-medium;
|
||||
}
|
||||
|
||||
.prompt-new-page .ant-card-body {
|
||||
@apply p-4;
|
||||
}
|
||||
@@ -166,6 +166,14 @@
|
||||
.rule-groups-page .ant-btn-text.text-primary {
|
||||
color: #00684a;
|
||||
}
|
||||
|
||||
.rule-groups-page .operations-cell {
|
||||
@apply flex space-x-2;
|
||||
}
|
||||
|
||||
.rule-groups-page .operation-btn {
|
||||
@apply text-sm flex items-center text-[--color-primary] bg-transparent hover:underline p-2;
|
||||
}
|
||||
|
||||
.rule-groups-page .ant-btn-text.text-primary:hover {
|
||||
color: #00684a;
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
/**
|
||||
* 评查点分组新增/编辑页样式
|
||||
*/
|
||||
|
||||
/* 页面容器 */
|
||||
.rule-group-new-page {
|
||||
@apply p-5;
|
||||
}
|
||||
|
||||
/* 页面标题区域 */
|
||||
.rule-group-new-page .page-header {
|
||||
@apply flex justify-between items-center mb-6;
|
||||
}
|
||||
|
||||
.rule-group-new-page .page-title {
|
||||
@apply text-xl font-medium mb-1;
|
||||
}
|
||||
|
||||
.rule-group-new-page .page-subtitle {
|
||||
@apply text-sm text-gray-500;
|
||||
}
|
||||
|
||||
/* 表单容器 */
|
||||
.rule-group-new-page .form-container {
|
||||
@apply p-0;
|
||||
}
|
||||
|
||||
/* 提示信息区域 */
|
||||
.rule-group-new-page .info-message {
|
||||
@apply flex items-center p-4 mb-5 bg-[rgba(0,104,74,0.05)] border border-[rgba(0,104,74,0.1)] rounded-md;
|
||||
}
|
||||
|
||||
.rule-group-new-page .info-message i {
|
||||
@apply text-lg mr-3 text-[#00684a];
|
||||
}
|
||||
|
||||
.rule-group-new-page .info-message p {
|
||||
@apply text-sm text-gray-600 m-0;
|
||||
}
|
||||
|
||||
/* 表单区域 */
|
||||
.rule-group-new-page .form-section {
|
||||
@apply bg-white rounded-md border border-gray-200 mb-6 transition-shadow duration-200 shadow-sm;
|
||||
}
|
||||
|
||||
.rule-group-new-page .form-section:hover {
|
||||
@apply shadow-md;
|
||||
}
|
||||
|
||||
.rule-group-new-page .form-section-header {
|
||||
@apply flex items-center p-4 border-b border-gray-100;
|
||||
}
|
||||
|
||||
.rule-group-new-page .form-section-header i {
|
||||
@apply text-lg mr-2 text-[#00684a];
|
||||
}
|
||||
|
||||
.rule-group-new-page .form-section-header h3 {
|
||||
@apply text-base font-medium m-0;
|
||||
}
|
||||
|
||||
.rule-group-new-page .form-section-body {
|
||||
@apply p-6;
|
||||
}
|
||||
|
||||
/* 表单元素 */
|
||||
.rule-group-new-page .form-group {
|
||||
@apply mb-5;
|
||||
}
|
||||
|
||||
.rule-group-new-page .form-label {
|
||||
@apply block text-sm font-medium mb-2 text-gray-700;
|
||||
}
|
||||
|
||||
.rule-group-new-page .form-label .required-mark {
|
||||
@apply text-red-500 ml-1;
|
||||
}
|
||||
|
||||
.rule-group-new-page .form-input,
|
||||
.rule-group-new-page .form-select,
|
||||
.rule-group-new-page .form-textarea {
|
||||
@apply w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-1 focus:ring-[#00684a] focus:border-[#00684a] outline-none text-sm transition-all duration-200;
|
||||
}
|
||||
|
||||
.rule-group-new-page .form-input:hover,
|
||||
.rule-group-new-page .form-select:hover,
|
||||
.rule-group-new-page .form-textarea:hover {
|
||||
@apply border-[#00684a];
|
||||
}
|
||||
|
||||
.rule-group-new-page .form-input.error,
|
||||
.rule-group-new-page .form-select.error,
|
||||
.rule-group-new-page .form-textarea.error {
|
||||
@apply border-red-500 focus:ring-0 ;
|
||||
}
|
||||
|
||||
.rule-group-new-page .form-textarea {
|
||||
@apply min-h-[120px] resize-y;
|
||||
}
|
||||
|
||||
.rule-group-new-page .form-tip {
|
||||
@apply mt-1.5 text-xs text-gray-500;
|
||||
}
|
||||
|
||||
.rule-group-new-page .form-error {
|
||||
@apply mt-1.5 text-xs text-red-500;
|
||||
}
|
||||
|
||||
/* 单选框组样式 */
|
||||
.rule-group-new-page .radio-group {
|
||||
@apply flex gap-6;
|
||||
}
|
||||
|
||||
.rule-group-new-page .radio-item {
|
||||
@apply flex items-center cursor-pointer;
|
||||
}
|
||||
|
||||
.rule-group-new-page .radio-input {
|
||||
@apply mr-2 cursor-pointer;
|
||||
}
|
||||
|
||||
/* 行布局 */
|
||||
.rule-group-new-page .form-row {
|
||||
@apply flex flex-col md:flex-row gap-6;
|
||||
}
|
||||
|
||||
.rule-group-new-page .form-col {
|
||||
@apply flex-1 min-w-0;
|
||||
}
|
||||
|
||||
/* 按钮组 */
|
||||
.rule-group-new-page .form-actions {
|
||||
@apply flex justify-end gap-3 mt-6 mb-10;
|
||||
}
|
||||
|
||||
/* 错误提示 */
|
||||
.rule-group-new-page .general-error {
|
||||
@apply p-4 mb-6 bg-red-50 border border-red-100 rounded-md text-red-700 text-sm;
|
||||
}
|
||||
|
||||
/* 响应式调整 */
|
||||
@media (max-width: 768px) {
|
||||
.rule-group-new-page .form-row {
|
||||
@apply flex-col;
|
||||
}
|
||||
|
||||
.rule-group-new-page .page-header {
|
||||
@apply flex-col items-start;
|
||||
}
|
||||
|
||||
.rule-group-new-page .page-header .header-actions {
|
||||
@apply mt-4;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user