Merge branch 'shiy' into awen
This commit is contained in:
@@ -7,13 +7,13 @@ import { FilterPanel, FilterSelect, SearchFilter } from "~/components/ui/FilterP
|
||||
import { Pagination } from "~/components/ui/Pagination";
|
||||
import { Table } from "~/components/ui/Table";
|
||||
import { Tag } from "~/components/ui/Tag";
|
||||
import { getConfigLists, getConfigOptions, updateConfigStatus, type ConfigItem } from "~/api/system_setting/config-lists";
|
||||
import configListsStyles from "~/styles/pages/config-lists_index.css?url";
|
||||
|
||||
export const links = () => [
|
||||
{ rel: "stylesheet", href: configListsStyles }
|
||||
];
|
||||
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return [
|
||||
{ title: "系统配置管理 - 中国烟草AI合同及卷宗审核系统" },
|
||||
@@ -54,165 +54,55 @@ export const MODULE_LABELS: Record<ConfigModule, string> = {
|
||||
[ConfigModule.NOTIFICATION]: '通知'
|
||||
};
|
||||
|
||||
// 配置数据类型
|
||||
interface ConfigDataType {
|
||||
[key: string]: string | number | boolean | string[] | ConfigDataType | ConfigDataType[];
|
||||
}
|
||||
|
||||
// 配置项模型
|
||||
interface ConfigItem {
|
||||
id: string;
|
||||
configName: string;
|
||||
module: ConfigModule;
|
||||
environment: ConfigEnvironment;
|
||||
isActive: boolean;
|
||||
configData: ConfigDataType;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
interface LoaderData {
|
||||
configs: ConfigItem[];
|
||||
totalCount: number;
|
||||
currentPage: number;
|
||||
pageSize: number;
|
||||
totalPages: number;
|
||||
types: string[];
|
||||
environments: string[];
|
||||
}
|
||||
|
||||
export async function loader({ request }: LoaderFunctionArgs) {
|
||||
const url = new URL(request.url);
|
||||
const configName = url.searchParams.get("configName") || "";
|
||||
const module = url.searchParams.get("module") || "";
|
||||
const name = url.searchParams.get("name") || "";
|
||||
const type = url.searchParams.get("type") || "";
|
||||
const environment = url.searchParams.get("environment") || "";
|
||||
const isActive = url.searchParams.get("isActive") || "";
|
||||
const is_active = url.searchParams.get("is_active") ? url.searchParams.get("is_active") === "true" : undefined;
|
||||
const currentPage = parseInt(url.searchParams.get("page") || "1", 10);
|
||||
const pageSize = parseInt(url.searchParams.get("pageSize") || "10", 10);
|
||||
|
||||
try {
|
||||
// 模拟数据,实际项目中应从API获取
|
||||
const mockConfigs: ConfigItem[] = [
|
||||
{
|
||||
id: "1",
|
||||
configName: "database_connection",
|
||||
module: ConfigModule.SYSTEM,
|
||||
environment: ConfigEnvironment.PROD,
|
||||
isActive: true,
|
||||
configData: {
|
||||
database: {
|
||||
host: "db.cluster.com",
|
||||
port: 5432,
|
||||
pool_size: 20,
|
||||
ssl: true
|
||||
},
|
||||
cache: {
|
||||
ttl: 3600,
|
||||
max_entries: 1000
|
||||
},
|
||||
feature_flags: ["new_ui", "analytics_v2"]
|
||||
},
|
||||
createdAt: "2023-07-10 10:15:23",
|
||||
updatedAt: "2023-07-15 14:30:26"
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
configName: "text_extraction_ai",
|
||||
module: ConfigModule.AI,
|
||||
environment: ConfigEnvironment.TEST,
|
||||
isActive: true,
|
||||
configData: {
|
||||
model: "gpt-4",
|
||||
parameters: {
|
||||
temperature: 0.7,
|
||||
max_tokens: 2000
|
||||
},
|
||||
api_key: "sk-**********",
|
||||
timeout: 30
|
||||
},
|
||||
createdAt: "2023-07-12 08:45:12",
|
||||
updatedAt: "2023-07-14 09:15:33"
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
configName: "notification_service",
|
||||
module: ConfigModule.NOTIFICATION,
|
||||
environment: ConfigEnvironment.DEV,
|
||||
isActive: false,
|
||||
configData: {
|
||||
email: {
|
||||
smtp_server: "smtp.example.com",
|
||||
port: 587,
|
||||
use_tls: true,
|
||||
sender: "noreply@example.com"
|
||||
},
|
||||
sms: {
|
||||
provider: "aliyun",
|
||||
region: "cn-hangzhou",
|
||||
sign_name: "AI审核系统"
|
||||
}
|
||||
},
|
||||
createdAt: "2023-07-05 13:20:45",
|
||||
updatedAt: "2023-07-10 16:45:19"
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
configName: "file_storage",
|
||||
module: ConfigModule.FILE,
|
||||
environment: ConfigEnvironment.PROD,
|
||||
isActive: true,
|
||||
configData: {
|
||||
type: "oss",
|
||||
region: "cn-shanghai",
|
||||
bucket: "contracts-ai-review",
|
||||
access_control: "private",
|
||||
lifecycle_rules: [
|
||||
{
|
||||
prefix: "temp/",
|
||||
ttl_days: 7
|
||||
}
|
||||
]
|
||||
},
|
||||
createdAt: "2023-06-28 09:30:18",
|
||||
updatedAt: "2023-07-08 11:22:07"
|
||||
}
|
||||
];
|
||||
|
||||
// 过滤数据
|
||||
let filteredConfigs = [...mockConfigs];
|
||||
|
||||
if (configName) {
|
||||
filteredConfigs = filteredConfigs.filter(config =>
|
||||
config.configName.toLowerCase().includes(configName.toLowerCase())
|
||||
);
|
||||
// 获取配置列表
|
||||
const configsResponse = await getConfigLists({
|
||||
name,
|
||||
type,
|
||||
environment,
|
||||
is_active,
|
||||
page: currentPage,
|
||||
pageSize
|
||||
});
|
||||
|
||||
if (configsResponse.error || !configsResponse.data) {
|
||||
throw new Error(configsResponse.error || "获取配置列表失败");
|
||||
}
|
||||
|
||||
if (module) {
|
||||
filteredConfigs = filteredConfigs.filter(config => config.module === module);
|
||||
|
||||
// 获取配置选项
|
||||
const optionsResponse = await getConfigOptions();
|
||||
|
||||
if (optionsResponse.error || !optionsResponse.data) {
|
||||
throw new Error(optionsResponse.error || "获取配置选项失败");
|
||||
}
|
||||
|
||||
if (environment) {
|
||||
filteredConfigs = filteredConfigs.filter(config => config.environment === environment);
|
||||
}
|
||||
|
||||
if (isActive) {
|
||||
const activeValue = isActive === 'true';
|
||||
filteredConfigs = filteredConfigs.filter(config => config.isActive === activeValue);
|
||||
}
|
||||
|
||||
// 计算分页信息
|
||||
const totalCount = filteredConfigs.length;
|
||||
const totalPages = Math.ceil(totalCount / pageSize);
|
||||
|
||||
// 分页截取
|
||||
const startIndex = (currentPage - 1) * pageSize;
|
||||
const endIndex = startIndex + pageSize;
|
||||
const paginatedConfigs = filteredConfigs.slice(startIndex, endIndex);
|
||||
|
||||
|
||||
return json<LoaderData>({
|
||||
configs: paginatedConfigs,
|
||||
totalCount,
|
||||
configs: configsResponse.data,
|
||||
totalCount: configsResponse.total,
|
||||
currentPage,
|
||||
pageSize,
|
||||
totalPages
|
||||
totalPages: Math.ceil(configsResponse.total / pageSize),
|
||||
types: optionsResponse.data.types,
|
||||
environments: optionsResponse.data.environments
|
||||
}, {
|
||||
headers: {
|
||||
"Cache-Control": "max-age=60, s-maxage=180"
|
||||
@@ -233,28 +123,18 @@ export async function action({ request }: ActionFunctionArgs) {
|
||||
return json({ success: false, error: "缺少配置ID" }, { status: 400 });
|
||||
}
|
||||
|
||||
// 进行更新启用和禁用的状态
|
||||
try {
|
||||
if (_action === 'toggleStatus') {
|
||||
const isActive = formData.get('isActive') === 'true';
|
||||
const newStatus = !isActive;
|
||||
const is_active = formData.get('is_active') === 'true';
|
||||
|
||||
// 实际项目中应调用API更新状态
|
||||
console.log(`切换配置 ${configId} 状态为: ${newStatus}`);
|
||||
const response = await updateConfigStatus(parseInt(configId as string), is_active);
|
||||
|
||||
// 模拟API调用
|
||||
// const response = await fetch(`/api/configs/${configId}/status`, {
|
||||
// method: 'PATCH',
|
||||
// headers: {
|
||||
// 'Content-Type': 'application/json',
|
||||
// },
|
||||
// body: JSON.stringify({ isActive: newStatus }),
|
||||
// });
|
||||
if (!response.success) {
|
||||
return json({ success: false, error: response.error }, { status: 500 });
|
||||
}
|
||||
|
||||
// if (!response.ok) {
|
||||
// throw new Error(`状态切换失败: ${response.status}`);
|
||||
// }
|
||||
|
||||
return json({ success: true, newStatus });
|
||||
return json({ success: true });
|
||||
}
|
||||
|
||||
return json({ success: false, error: "未知操作" }, { status: 400 });
|
||||
@@ -275,7 +155,7 @@ export function ErrorBoundary() {
|
||||
}
|
||||
|
||||
export default function ConfigListsIndex() {
|
||||
const { configs, totalCount, currentPage, pageSize } = useLoaderData<typeof loader>();
|
||||
const { configs, totalCount, currentPage, pageSize, types, environments } = useLoaderData<typeof loader>();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const submit = useSubmit();
|
||||
const [showDetailModal, setShowDetailModal] = useState(false);
|
||||
@@ -300,9 +180,9 @@ export default function ConfigListsIndex() {
|
||||
const handleConfigNameSearch = (value: string) => {
|
||||
const newParams = new URLSearchParams(searchParams);
|
||||
if (value) {
|
||||
newParams.set('configName', value);
|
||||
newParams.set('name', value);
|
||||
} else {
|
||||
newParams.delete('configName');
|
||||
newParams.delete('name');
|
||||
}
|
||||
|
||||
// 搜索时,重置到第一页
|
||||
@@ -312,11 +192,11 @@ export default function ConfigListsIndex() {
|
||||
};
|
||||
|
||||
const handleToggleStatus = (config: ConfigItem) => {
|
||||
if (window.confirm(`确定要${config.isActive ? '禁用' : '启用'}该配置吗?`)) {
|
||||
if (window.confirm(`确定要${config.is_active ? '禁用' : '启用'}该配置吗?`)) {
|
||||
const formData = new FormData();
|
||||
formData.append('_action', 'toggleStatus');
|
||||
formData.append('configId', config.id);
|
||||
formData.append('isActive', String(config.isActive));
|
||||
formData.append('configId', config.id.toString());
|
||||
formData.append('is_active', String(!config.is_active));
|
||||
|
||||
submit(formData, { method: 'post' });
|
||||
}
|
||||
@@ -342,10 +222,20 @@ export default function ConfigListsIndex() {
|
||||
|
||||
// 处理重置筛选
|
||||
const handleReset = () => {
|
||||
const nameInput = document.querySelector('input[placeholder="请输入配置名称"]') as HTMLInputElement;
|
||||
const typeSelect = document.querySelector('select[name="type"]') as HTMLInputElement;
|
||||
const environmentSelect = document.querySelector('select[name="environment"]') as HTMLInputElement;
|
||||
const statusSelect = document.querySelector('select[name="is_active"]') as HTMLInputElement;
|
||||
|
||||
setSearchParams(new URLSearchParams());
|
||||
|
||||
if(nameInput) nameInput.value = ''
|
||||
if(typeSelect) typeSelect.value = ''
|
||||
if(environmentSelect) environmentSelect.value = ''
|
||||
if(statusSelect) statusSelect.value = ''
|
||||
|
||||
};
|
||||
|
||||
|
||||
// 关闭详情模态框
|
||||
const closeDetailModal = () => {
|
||||
setShowDetailModal(false);
|
||||
@@ -356,43 +246,42 @@ export default function ConfigListsIndex() {
|
||||
const columns = [
|
||||
{
|
||||
title: "配置名称",
|
||||
dataIndex: "configName" as keyof ConfigItem,
|
||||
key: "configName",
|
||||
dataIndex: "name" as keyof ConfigItem,
|
||||
key: "name",
|
||||
width: "20%"
|
||||
},
|
||||
{
|
||||
title: "所属模块",
|
||||
key: "module",
|
||||
key: "type",
|
||||
width: "10%",
|
||||
render: (_: unknown, record: ConfigItem) => MODULE_LABELS[record.module]
|
||||
render: (_: unknown, record: ConfigItem) => record.type
|
||||
},
|
||||
{
|
||||
title: "环境",
|
||||
key: "environment",
|
||||
width: "15%",
|
||||
render: (_: unknown, record: ConfigItem) => {
|
||||
const envClass = `env-tag env-tag-${record.environment}`;
|
||||
return (
|
||||
<span className={envClass}>
|
||||
{ENVIRONMENT_LABELS[record.environment]}
|
||||
<span className="env-tag">
|
||||
{record.environment}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "状态",
|
||||
key: "isActive",
|
||||
key: "is_active",
|
||||
width: "15%",
|
||||
render: (_: unknown, record: ConfigItem) => (
|
||||
<Tag color={record.isActive ? 'green' : 'red'}>
|
||||
{record.isActive ? '已启用' : '已禁用'}
|
||||
<Tag color={record.is_active ? 'green' : 'red'}>
|
||||
{record.is_active ? '已启用' : '已禁用'}
|
||||
</Tag>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: "最后更新时间",
|
||||
dataIndex: "updatedAt" as keyof ConfigItem,
|
||||
key: "updatedAt",
|
||||
dataIndex: "updated_at" as keyof ConfigItem,
|
||||
key: "updated_at",
|
||||
width: "15%"
|
||||
},
|
||||
{
|
||||
@@ -417,29 +306,17 @@ export default function ConfigListsIndex() {
|
||||
</Link>
|
||||
<button
|
||||
type="button"
|
||||
className={`operation-btn ${record.isActive ? '!text-[--color-warning]' : '!text-[--color-success]'}`}
|
||||
className={`operation-btn ${record.is_active ? '!text-[--color-warning]' : '!text-[--color-success]'}`}
|
||||
onClick={() => handleToggleStatus(record)}
|
||||
>
|
||||
<i className={record.isActive ? `ri-stop-circle-line` : `ri-play-circle-line`}></i>
|
||||
{record.isActive ? '禁用' : '启用'}
|
||||
<i className={record.is_active ? `ri-stop-circle-line` : `ri-play-circle-line`}></i>
|
||||
{record.is_active ? '禁用' : '启用'}
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
// 生成环境选项
|
||||
const environmentOptions = Object.entries(ENVIRONMENT_LABELS).map(([value, label]) => ({
|
||||
value,
|
||||
label
|
||||
}));
|
||||
|
||||
// 生成模块选项
|
||||
const moduleOptions = Object.entries(MODULE_LABELS).map(([value, label]) => ({
|
||||
value,
|
||||
label
|
||||
}));
|
||||
|
||||
return (
|
||||
<div className="config-lists">
|
||||
{/* 页面头部 */}
|
||||
@@ -458,9 +335,9 @@ export default function ConfigListsIndex() {
|
||||
<Button type="default" icon="ri-refresh-line" onClick={handleReset} className="mr-2">
|
||||
重置
|
||||
</Button>
|
||||
<Button type="primary" icon="ri-search-line">
|
||||
{/* <Button type="primary" icon="ri-search-line">
|
||||
查询
|
||||
</Button>
|
||||
</Button> */}
|
||||
</>
|
||||
}
|
||||
noActionDivider={true}
|
||||
@@ -468,7 +345,7 @@ export default function ConfigListsIndex() {
|
||||
<SearchFilter
|
||||
label="配置名称"
|
||||
placeholder="请输入配置名称"
|
||||
value={searchParams.get('configName') || ''}
|
||||
value={searchParams.get('name') || ''}
|
||||
onSearch={handleConfigNameSearch}
|
||||
className="flex-1 min-w-[200px]"
|
||||
instantSearch={true}
|
||||
@@ -476,9 +353,9 @@ export default function ConfigListsIndex() {
|
||||
|
||||
<FilterSelect
|
||||
label="所属模块"
|
||||
name="module"
|
||||
value={searchParams.get('module') || ''}
|
||||
options={[{ value: '', label: '全部' }, ...moduleOptions]}
|
||||
name="type"
|
||||
value={searchParams.get('type') || ''}
|
||||
options={[ ...types.map(type => ({ value: type, label: type }))]}
|
||||
onChange={handleFilterChange}
|
||||
className="flex-1 min-w-[200px]"
|
||||
/>
|
||||
@@ -487,17 +364,16 @@ export default function ConfigListsIndex() {
|
||||
label="环境"
|
||||
name="environment"
|
||||
value={searchParams.get('environment') || ''}
|
||||
options={[{ value: '', label: '全部' }, ...environmentOptions]}
|
||||
options={[ ...environments.map(env => ({ value: env, label: env }))]}
|
||||
onChange={handleFilterChange}
|
||||
className="flex-1 min-w-[200px]"
|
||||
/>
|
||||
|
||||
<FilterSelect
|
||||
label="状态"
|
||||
name="isActive"
|
||||
value={searchParams.get('isActive') || ''}
|
||||
name="is_active"
|
||||
value={searchParams.get('is_active') || ''}
|
||||
options={[
|
||||
{ value: '', label: '全部' },
|
||||
{ value: 'true', label: '已启用' },
|
||||
{ value: 'false', label: '已禁用' }
|
||||
]}
|
||||
@@ -545,19 +421,19 @@ export default function ConfigListsIndex() {
|
||||
<div className="config-detail-content">
|
||||
<div className="config-detail-item">
|
||||
<div className="config-detail-label">配置名称</div>
|
||||
<div className="config-detail-value">{selectedConfig.configName}</div>
|
||||
<div className="config-detail-value">{selectedConfig.name}</div>
|
||||
</div>
|
||||
|
||||
<div className="config-detail-item">
|
||||
<div className="config-detail-label">所属模块</div>
|
||||
<div className="config-detail-value">{MODULE_LABELS[selectedConfig.module]}</div>
|
||||
<div className="config-detail-value">{selectedConfig.type}</div>
|
||||
</div>
|
||||
|
||||
<div className="config-detail-item">
|
||||
<div className="config-detail-label">环境</div>
|
||||
<div className="config-detail-value">
|
||||
<span className={`env-tag env-tag-${selectedConfig.environment}`}>
|
||||
{ENVIRONMENT_LABELS[selectedConfig.environment]}
|
||||
<span className="env-tag">
|
||||
{selectedConfig.environment}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -565,8 +441,8 @@ export default function ConfigListsIndex() {
|
||||
<div className="config-detail-item">
|
||||
<div className="config-detail-label">状态</div>
|
||||
<div className="config-detail-value">
|
||||
<Tag color={selectedConfig.isActive ? 'green' : 'red'}>
|
||||
{selectedConfig.isActive ? '已启用' : '已禁用'}
|
||||
<Tag color={selectedConfig.is_active ? 'green' : 'red'}>
|
||||
{selectedConfig.is_active ? '已启用' : '已禁用'}
|
||||
</Tag>
|
||||
</div>
|
||||
</div>
|
||||
@@ -574,19 +450,19 @@ export default function ConfigListsIndex() {
|
||||
<div className="config-detail-item">
|
||||
<div className="config-detail-label">配置数据</div>
|
||||
<pre className="config-detail-code">
|
||||
{JSON.stringify(selectedConfig.configData, null, 2)}
|
||||
{JSON.stringify(selectedConfig.config, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="config-detail-item">
|
||||
<div className="config-detail-label">创建时间</div>
|
||||
<div className="config-detail-value">{selectedConfig.createdAt}</div>
|
||||
<div className="config-detail-value">{selectedConfig.created_at}</div>
|
||||
</div>
|
||||
|
||||
<div className="config-detail-item">
|
||||
<div className="config-detail-label">更新时间</div>
|
||||
<div className="config-detail-value">{selectedConfig.updatedAt}</div>
|
||||
<div className="config-detail-value">{selectedConfig.updated_at}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+174
-284
@@ -4,6 +4,7 @@ import { useEffect, useState } from "react";
|
||||
import { Button } from "~/components/ui/Button";
|
||||
import { Card } from "~/components/ui/Card";
|
||||
import { ConfigModule, MODULE_LABELS, ENVIRONMENT_LABELS } from "./config-lists._index";
|
||||
import { getConfigOptions, getConfigDetail, createConfig, updateConfig } from "~/api/system_setting/config-lists";
|
||||
import configNewStyles from "~/styles/pages/config-lists_new.css?url";
|
||||
|
||||
export const links = () => [
|
||||
@@ -39,18 +40,20 @@ export const EXTENDED_ENVIRONMENT_LABELS: Record<string, string> = {
|
||||
};
|
||||
|
||||
interface ConfigData {
|
||||
id: string;
|
||||
configName: string;
|
||||
module: ConfigModule;
|
||||
environment: string; // 使用扩展的环境类型
|
||||
isActive: boolean;
|
||||
configData: string; // JSON字符串
|
||||
remarks?: string; // 添加备注字段
|
||||
id: number;
|
||||
name: string;
|
||||
type: string;
|
||||
environment: string;
|
||||
is_active: boolean;
|
||||
config: Record<string, unknown>;
|
||||
remark?: string;
|
||||
}
|
||||
|
||||
interface LoaderData {
|
||||
config?: ConfigData;
|
||||
isEdit: boolean;
|
||||
types: string[];
|
||||
environments: string[];
|
||||
}
|
||||
|
||||
export async function loader({ request }: LoaderFunctionArgs) {
|
||||
@@ -58,193 +61,107 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
const id = url.searchParams.get("id");
|
||||
let config: ConfigData | undefined = undefined;
|
||||
|
||||
// 获取配置选项
|
||||
const optionsResponse = await getConfigOptions();
|
||||
if (optionsResponse.error) {
|
||||
throw new Error(optionsResponse.error);
|
||||
}
|
||||
|
||||
if (id) {
|
||||
try {
|
||||
// 实际应用中,应从API获取配置详情
|
||||
// const response = await fetch(`${process.env.API_BASE_URL}/api/configs/${id}`);
|
||||
// if (!response.ok) throw new Error(`获取配置详情失败: ${response.status}`);
|
||||
// config = await response.json();
|
||||
// config.configData = JSON.stringify(config.configData, null, 2);
|
||||
|
||||
// 使用模拟数据
|
||||
if (id === "1") {
|
||||
config = {
|
||||
id: "1",
|
||||
configName: "database_connection",
|
||||
module: ConfigModule.SYSTEM,
|
||||
environment: ExtendedConfigEnvironment.PROD,
|
||||
isActive: true,
|
||||
remarks: "数据库连接配置,包含主库和从库配置",
|
||||
configData: JSON.stringify({
|
||||
database: {
|
||||
host: "db.cluster.com",
|
||||
port: 5432,
|
||||
pool_size: 20,
|
||||
ssl: true
|
||||
},
|
||||
cache: {
|
||||
ttl: 3600,
|
||||
max_entries: 1000
|
||||
},
|
||||
feature_flags: ["new_ui", "analytics_v2"]
|
||||
}, null, 2)
|
||||
};
|
||||
} else if (id === "2") {
|
||||
config = {
|
||||
id: "2",
|
||||
configName: "text_extraction_ai",
|
||||
module: ConfigModule.AI,
|
||||
environment: ExtendedConfigEnvironment.TEST,
|
||||
isActive: true,
|
||||
remarks: "AI文本抽取服务配置",
|
||||
configData: JSON.stringify({
|
||||
model: "gpt-4",
|
||||
parameters: {
|
||||
temperature: 0.7,
|
||||
max_tokens: 2000
|
||||
},
|
||||
api_key: "sk-**********",
|
||||
timeout: 30
|
||||
}, null, 2)
|
||||
};
|
||||
} else if (id === "3") {
|
||||
config = {
|
||||
id: "3",
|
||||
configName: "notification_service",
|
||||
module: ConfigModule.NOTIFICATION,
|
||||
environment: ExtendedConfigEnvironment.DEV,
|
||||
isActive: false,
|
||||
remarks: "通知服务配置,目前处于开发测试阶段",
|
||||
configData: JSON.stringify({
|
||||
email: {
|
||||
smtp_server: "smtp.example.com",
|
||||
port: 587,
|
||||
use_tls: true,
|
||||
sender: "noreply@example.com"
|
||||
},
|
||||
sms: {
|
||||
provider: "aliyun",
|
||||
region: "cn-hangzhou",
|
||||
sign_name: "AI审核系统"
|
||||
}
|
||||
}, null, 2)
|
||||
};
|
||||
} else if (id === "4") {
|
||||
config = {
|
||||
id: "4",
|
||||
configName: "file_storage",
|
||||
module: ConfigModule.FILE,
|
||||
environment: ExtendedConfigEnvironment.COMMON,
|
||||
isActive: true,
|
||||
remarks: "文件存储通用配置,适用于所有环境",
|
||||
configData: JSON.stringify({
|
||||
type: "oss",
|
||||
region: "cn-shanghai",
|
||||
bucket: "contracts-ai-review",
|
||||
access_control: "private",
|
||||
lifecycle_rules: [
|
||||
{
|
||||
prefix: "temp/",
|
||||
ttl_days: 7
|
||||
}
|
||||
]
|
||||
}, null, 2)
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("获取配置详情失败:", error);
|
||||
// 在实际应用中,应该将错误信息返回给客户端
|
||||
// 这里简单处理,返回空config
|
||||
// 获取配置详情
|
||||
const detailResponse = await getConfigDetail(id);
|
||||
if (detailResponse.error) {
|
||||
throw new Error(detailResponse.error);
|
||||
}
|
||||
config = detailResponse.data;
|
||||
}
|
||||
|
||||
return Response.json({
|
||||
config,
|
||||
isEdit: !!config
|
||||
isEdit: !!config,
|
||||
types: optionsResponse.data?.types || [],
|
||||
environments: optionsResponse.data?.environments || []
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
interface ActionData {
|
||||
success?: boolean;
|
||||
errors?: {
|
||||
configName?: string;
|
||||
module?: string;
|
||||
name?: string;
|
||||
type?: string;
|
||||
environment?: string;
|
||||
configData?: string;
|
||||
config?: string;
|
||||
general?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export async function action({ request }: ActionFunctionArgs) {
|
||||
const formData = await request.formData();
|
||||
const configId = formData.get("id") as string;
|
||||
const configName = formData.get("configName") as string;
|
||||
const module = formData.get("module") as string;
|
||||
const id = formData.get("id") as string;
|
||||
const name = formData.get("name") as string;
|
||||
const type = formData.get("type") as string;
|
||||
const environment = formData.get("environment") as string;
|
||||
const configData = formData.get("configData") as string;
|
||||
const isActive = formData.get("isActive") === "true";
|
||||
const remarks = formData.get("remarks") as string;
|
||||
const config = formData.get("config") as string;
|
||||
const is_active = formData.get("is_active") === "true";
|
||||
const remark = formData.get("remark") as string;
|
||||
|
||||
const errors: ActionData["errors"] = {};
|
||||
|
||||
// 表单验证
|
||||
if (!configName || configName.trim() === "") {
|
||||
errors.configName = "配置名称不能为空";
|
||||
if (!name || name.trim() === "") {
|
||||
errors.name = "配置名称不能为空";
|
||||
}
|
||||
|
||||
if (!module) {
|
||||
errors.module = "请选择所属模块";
|
||||
if (!type) {
|
||||
errors.type = "请选择所属模块";
|
||||
}
|
||||
|
||||
if (!environment) {
|
||||
errors.environment = "请选择环境";
|
||||
}
|
||||
|
||||
if (!configData || configData.trim() === "") {
|
||||
errors.configData = "配置数据不能为空";
|
||||
if (!config || config.trim() === "") {
|
||||
errors.config = "配置数据不能为空";
|
||||
} else {
|
||||
try {
|
||||
JSON.parse(configData);
|
||||
JSON.parse(config);
|
||||
} catch (e) {
|
||||
errors.configData = "配置数据必须是有效的JSON格式";
|
||||
errors.config = "配置数据必须是有效的JSON格式";
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(errors).length > 0) {
|
||||
return json<ActionData>({ errors });
|
||||
return Response.json({ errors });
|
||||
}
|
||||
|
||||
try {
|
||||
// 实际应用中,应调用API保存数据
|
||||
console.log("保存配置:", { configId, configName, module, environment, configData, isActive, remarks });
|
||||
const configData = {
|
||||
name,
|
||||
type,
|
||||
environment,
|
||||
config: JSON.parse(config),
|
||||
is_active,
|
||||
remark
|
||||
};
|
||||
|
||||
// 模拟API调用
|
||||
// const response = await fetch(`${process.env.API_BASE_URL}/api/configs${configId ? `/${configId}` : ''}`, {
|
||||
// method: configId ? "PUT" : "POST",
|
||||
// headers: {
|
||||
// "Content-Type": "application/json",
|
||||
// },
|
||||
// body: JSON.stringify({
|
||||
// id: configId,
|
||||
// configName,
|
||||
// module,
|
||||
// environment,
|
||||
// configData: JSON.parse(configData),
|
||||
// isActive,
|
||||
// remarks,
|
||||
// }),
|
||||
// });
|
||||
//
|
||||
// if (!response.ok) {
|
||||
// throw new Error(`保存失败: ${response.status}`);
|
||||
// }
|
||||
if (id) {
|
||||
// 更新配置
|
||||
const response = await updateConfig(id, configData);
|
||||
if (response.error) {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
} else {
|
||||
// 创建配置
|
||||
const response = await createConfig(configData);
|
||||
if (response.error) {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
}
|
||||
|
||||
// 保存成功后重定向到列表页
|
||||
return redirect("/config-lists");
|
||||
} catch (error) {
|
||||
console.error("保存配置失败:", error);
|
||||
return json<ActionData>({
|
||||
return Response.json({
|
||||
success: false,
|
||||
errors: {
|
||||
general: "保存配置失败,请稍后重试"
|
||||
@@ -253,18 +170,17 @@ export async function action({ request }: ActionFunctionArgs) {
|
||||
}
|
||||
}
|
||||
|
||||
// JSON模板数据
|
||||
const JSON_TEMPLATES = {
|
||||
// 配置模板常量
|
||||
const CONFIG_TEMPLATES = {
|
||||
database: {
|
||||
database: {
|
||||
host: "localhost",
|
||||
port: 5432,
|
||||
username: "db_user",
|
||||
password: "******",
|
||||
name: "app_database",
|
||||
pool_size: 10,
|
||||
timeout: 5000,
|
||||
ssl: false
|
||||
host: "localhost",
|
||||
port: 5432,
|
||||
database: "mydb",
|
||||
username: "admin",
|
||||
password: "******",
|
||||
pool: {
|
||||
min: 2,
|
||||
max: 10
|
||||
}
|
||||
},
|
||||
file: {
|
||||
@@ -290,8 +206,7 @@ const JSON_TEMPLATES = {
|
||||
};
|
||||
|
||||
export default function ConfigNew() {
|
||||
const { config, isEdit } = useLoaderData<typeof loader>();
|
||||
|
||||
const { config, isEdit, types, environments } = useLoaderData<typeof loader>();
|
||||
const actionData = useActionData<typeof action>();
|
||||
const navigation = useNavigation();
|
||||
const isSubmitting = navigation.state === "submitting";
|
||||
@@ -304,18 +219,14 @@ export default function ConfigNew() {
|
||||
const [selectedModule, setSelectedModule] = useState<string>("");
|
||||
const [selectedEnvironment, setSelectedEnvironment] = useState<string>("");
|
||||
|
||||
// 在 ConfigNew 组件中添加状态来跟踪当前选中的模板
|
||||
const [selectedTemplate, setSelectedTemplate] = useState<keyof typeof CONFIG_TEMPLATES | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
// 初始化配置数据
|
||||
if (config?.configData) {
|
||||
setConfigDataValue(config.configData);
|
||||
}
|
||||
|
||||
// 初始化模块和环境的选中状态
|
||||
if (config?.module) {
|
||||
setSelectedModule(config.module);
|
||||
}
|
||||
|
||||
if (config?.environment) {
|
||||
if (config) {
|
||||
setConfigDataValue(JSON.stringify(config.config, null, 2));
|
||||
setSelectedModule(config.type);
|
||||
setSelectedEnvironment(config.environment);
|
||||
}
|
||||
|
||||
@@ -333,7 +244,6 @@ export default function ConfigNew() {
|
||||
},
|
||||
feature_flags: ["new_ui", "analytics_v2"]
|
||||
}, null, 2));
|
||||
|
||||
}, [config]);
|
||||
|
||||
// 处理JSON数据变更
|
||||
@@ -360,6 +270,7 @@ export default function ConfigNew() {
|
||||
|
||||
// 格式化JSON
|
||||
const handleFormatJson = () => {
|
||||
|
||||
if (configDataValue.trim() === "") return;
|
||||
|
||||
try {
|
||||
@@ -375,60 +286,6 @@ export default function ConfigNew() {
|
||||
}
|
||||
};
|
||||
|
||||
// 加载JSON模板
|
||||
const handleLoadTemplate = (type: keyof typeof JSON_TEMPLATES) => {
|
||||
const template = JSON_TEMPLATES[type];
|
||||
setConfigDataValue(JSON.stringify(template, null, 2));
|
||||
setJsonError(null);
|
||||
};
|
||||
|
||||
// 模块标签点击
|
||||
const handleModuleTagClick = (module: string) => {
|
||||
setSelectedModule(module);
|
||||
};
|
||||
|
||||
// 环境标签点击
|
||||
const handleEnvironmentTagClick = (env: string) => {
|
||||
setSelectedEnvironment(env);
|
||||
};
|
||||
|
||||
// 显示JSON语法高亮
|
||||
const renderJsonWithSyntaxHighlight = (json: string) => {
|
||||
try {
|
||||
// 如果是空字符串,直接返回
|
||||
if (!json.trim()) return "";
|
||||
|
||||
// 解析并格式化JSON
|
||||
const parsed = JSON.parse(json);
|
||||
const formatted = JSON.stringify(parsed, null, 2);
|
||||
|
||||
// 添加语法高亮
|
||||
return formatted
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+-]?\d+)?)/g, (match) => {
|
||||
let cls = 'number';
|
||||
if (/^"/.test(match)) {
|
||||
if (/:$/.test(match)) {
|
||||
cls = 'key';
|
||||
match = match.replace(':', '');
|
||||
} else {
|
||||
cls = 'string';
|
||||
}
|
||||
} else if (/true|false/.test(match)) {
|
||||
cls = 'boolean';
|
||||
} else if (/null/.test(match)) {
|
||||
cls = 'null';
|
||||
}
|
||||
return `<span class="code-json ${cls}">${match}</span>`;
|
||||
});
|
||||
} catch (e) {
|
||||
// 如果解析失败,返回原始JSON
|
||||
return json;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="config-new-page">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
@@ -444,6 +301,12 @@ export default function ConfigNew() {
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{actionData?.errors?.general && (
|
||||
<div className="mb-4 w-full">
|
||||
<div className="error-message general-error">{actionData.errors.general}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Card className="config-form-card">
|
||||
<Form method="post" id="configForm" className="config-form">
|
||||
@@ -452,18 +315,18 @@ export default function ConfigNew() {
|
||||
{/* 配置名称和状态 */}
|
||||
<div className="form-row">
|
||||
<div className="form-group">
|
||||
<label htmlFor="configName" className="form-label required">配置名称</label>
|
||||
<label htmlFor="name" className="form-label required">配置名称</label>
|
||||
<input
|
||||
type="text"
|
||||
id="configName"
|
||||
name="configName"
|
||||
className={`form-input ${actionData?.errors?.configName ? 'input-error' : ''}`}
|
||||
defaultValue={config?.configName || ''}
|
||||
id="name"
|
||||
name="name"
|
||||
className={`form-input ${actionData?.errors?.name ? 'input-error' : ''}`}
|
||||
defaultValue={config?.name || ''}
|
||||
placeholder="请输入配置名称,如database_connection"
|
||||
required
|
||||
/>
|
||||
{actionData?.errors?.configName && (
|
||||
<div className="error-message">{actionData.errors.configName}</div>
|
||||
{actionData?.errors?.name && (
|
||||
<div className="error-message">{actionData.errors.name}</div>
|
||||
)}
|
||||
<div className="form-help">
|
||||
唯一标识符,配置名称应使用英文,推荐使用下划线命名方式
|
||||
@@ -471,18 +334,18 @@ export default function ConfigNew() {
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="isActive" className="form-label">状态</label>
|
||||
<label htmlFor="is_active" className="form-label">状态</label>
|
||||
<div className="mt-2">
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="isActive"
|
||||
name="isActive"
|
||||
id="is_active"
|
||||
name="is_active"
|
||||
value="true"
|
||||
className="form-checkbox"
|
||||
defaultChecked={config?.isActive !== false}
|
||||
defaultChecked={config?.is_active !== false}
|
||||
/>
|
||||
<label htmlFor="isActive" className="form-checkbox-label">
|
||||
<label htmlFor="is_active" className="form-checkbox-label">
|
||||
启用此配置
|
||||
</label>
|
||||
</div>
|
||||
@@ -495,33 +358,33 @@ export default function ConfigNew() {
|
||||
|
||||
{/* 所属模块 */}
|
||||
<div className="form-group">
|
||||
<label htmlFor="module" className="form-label required">所属模块</label>
|
||||
<label htmlFor="type" className="form-label required">所属模块</label>
|
||||
<input
|
||||
type="hidden"
|
||||
name="module"
|
||||
name="type"
|
||||
value={selectedModule}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
id="moduleDisplay"
|
||||
className={`form-input ${actionData?.errors?.module ? 'input-error' : ''}`}
|
||||
value={selectedModule ? MODULE_LABELS[selectedModule as ConfigModule] || selectedModule : ''}
|
||||
id="typeDisplay"
|
||||
className={`form-input ${actionData?.errors?.type ? 'input-error' : ''}`}
|
||||
value={selectedModule}
|
||||
onChange={(e) => setSelectedModule(e.target.value)}
|
||||
placeholder="请输入或选择所属模块"
|
||||
readOnly
|
||||
required
|
||||
/>
|
||||
{actionData?.errors?.module && (
|
||||
<div className="error-message">{actionData.errors.module}</div>
|
||||
{actionData?.errors?.type && (
|
||||
<div className="error-message">{actionData.errors.type}</div>
|
||||
)}
|
||||
<div className="tag-buttons mt-2">
|
||||
{Object.entries(MODULE_LABELS).map(([value, label]) => (
|
||||
{types.map((type: string) => (
|
||||
<button
|
||||
key={value}
|
||||
key={type}
|
||||
type="button"
|
||||
className={`tag-button ${selectedModule === value ? 'active' : ''}`}
|
||||
onClick={() => handleModuleTagClick(value)}
|
||||
className={`tag-button ${selectedModule === type ? 'active' : ''}`}
|
||||
onClick={() => setSelectedModule(type)}
|
||||
>
|
||||
{label}
|
||||
{type}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
@@ -542,23 +405,23 @@ export default function ConfigNew() {
|
||||
type="text"
|
||||
id="environmentDisplay"
|
||||
className={`form-input ${actionData?.errors?.environment ? 'input-error' : ''}`}
|
||||
value={selectedEnvironment ? EXTENDED_ENVIRONMENT_LABELS[selectedEnvironment] || selectedEnvironment : ''}
|
||||
value={selectedEnvironment}
|
||||
onChange={(e) => setSelectedEnvironment(e.target.value)}
|
||||
placeholder="请输入或选择环境"
|
||||
readOnly
|
||||
required
|
||||
/>
|
||||
{actionData?.errors?.environment && (
|
||||
<div className="error-message">{actionData.errors.environment}</div>
|
||||
)}
|
||||
<div className="tag-buttons mt-2">
|
||||
{Object.entries(EXTENDED_ENVIRONMENT_LABELS).map(([value, label]) => (
|
||||
{environments.map((env: string) => (
|
||||
<button
|
||||
key={value}
|
||||
key={env}
|
||||
type="button"
|
||||
className={`tag-button ${selectedEnvironment === value ? 'active' : ''}`}
|
||||
onClick={() => handleEnvironmentTagClick(value)}
|
||||
className={`tag-button ${selectedEnvironment === env ? 'active' : ''}`}
|
||||
onClick={() => setSelectedEnvironment(env)}
|
||||
>
|
||||
{label}
|
||||
{env}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
@@ -569,14 +432,14 @@ export default function ConfigNew() {
|
||||
|
||||
{/* 配置数据 */}
|
||||
<div className="form-group">
|
||||
<label htmlFor="configData" className="form-label required">配置数据 (JSON)</label>
|
||||
<label htmlFor="config" className="form-label required">配置数据 (JSON)</label>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4" style={{ minHeight: '390px' }}>
|
||||
{/* 左侧JSON编辑区 */}
|
||||
<div className="h-full">
|
||||
<textarea
|
||||
id="configData"
|
||||
name="configData"
|
||||
className={`json-editor ${(actionData?.errors?.configData || jsonError) ? 'input-error' : ''}`}
|
||||
id="config"
|
||||
name="config"
|
||||
className={`json-editor ${(actionData?.errors?.config || jsonError) ? 'input-error' : ''}`}
|
||||
value={configDataValue}
|
||||
onChange={handleConfigDataChange}
|
||||
required
|
||||
@@ -586,13 +449,16 @@ export default function ConfigNew() {
|
||||
<Button
|
||||
type="default"
|
||||
size="small"
|
||||
onClick={handleFormatJson}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
handleFormatJson();
|
||||
}}
|
||||
>
|
||||
<i className="ri-braces-line mr-1"></i> 格式化JSON
|
||||
</Button>
|
||||
</div>
|
||||
{(actionData?.errors?.configData || jsonError) && (
|
||||
<div className="error-message">{actionData?.errors?.configData || jsonError}</div>
|
||||
{(actionData?.errors?.config || jsonError) && (
|
||||
<div className="error-message">{actionData?.errors?.config || jsonError}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -603,32 +469,62 @@ export default function ConfigNew() {
|
||||
<div className="example-title">配置示例</div>
|
||||
</div>
|
||||
<div className="example-content">
|
||||
<pre
|
||||
className="example-pre"
|
||||
dangerouslySetInnerHTML={{ __html: renderJsonWithSyntaxHighlight(exampleJsonValue) }}
|
||||
/>
|
||||
<div className="json-display">
|
||||
{exampleJsonValue.split('\n').map((line, index) => (
|
||||
<div key={index} className="json-line">
|
||||
{line.split('').map((char, charIndex) => {
|
||||
let className = 'json-char';
|
||||
if (char === '{' || char === '}') className += ' json-brace';
|
||||
if (char === '[' || char === ']') className += ' json-bracket';
|
||||
if (char === '"') className += ' json-quote';
|
||||
if (char === ':') className += ' json-colon';
|
||||
if (char === ',') className += ' json-comma';
|
||||
return (
|
||||
<span key={charIndex} className={className}>
|
||||
{char}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="example-footer">
|
||||
<div className="text-sm font-medium mb-2">常用配置模板:</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button
|
||||
type="default"
|
||||
type="default"
|
||||
size="small"
|
||||
onClick={() => handleLoadTemplate('database')}
|
||||
className={`${selectedTemplate === 'database' ? 'border-[#00684a] text-[#00684a] focus:ring-0' : ''}`}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setExampleJsonValue(JSON.stringify(CONFIG_TEMPLATES.database, null, 2));
|
||||
setSelectedTemplate('database');
|
||||
}}
|
||||
>
|
||||
数据库配置
|
||||
</Button>
|
||||
<Button
|
||||
type="default"
|
||||
type="default"
|
||||
size="small"
|
||||
onClick={() => handleLoadTemplate('file')}
|
||||
className={`${selectedTemplate === 'file' ? 'border-[#00684a] text-[#00684a] focus:ring-0' : ''}`}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setExampleJsonValue(JSON.stringify(CONFIG_TEMPLATES.file, null, 2));
|
||||
setSelectedTemplate('file');
|
||||
}}
|
||||
>
|
||||
文件存储配置
|
||||
</Button>
|
||||
<Button
|
||||
type="default"
|
||||
type="default"
|
||||
size="small"
|
||||
onClick={() => handleLoadTemplate('ai')}
|
||||
className={`${selectedTemplate === 'ai' ? 'border-[#00684a] text-[#00684a] focus:ring-0' : ''}`}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setExampleJsonValue(JSON.stringify(CONFIG_TEMPLATES.ai, null, 2));
|
||||
setSelectedTemplate('ai');
|
||||
}}
|
||||
>
|
||||
AI服务配置
|
||||
</Button>
|
||||
@@ -644,12 +540,12 @@ export default function ConfigNew() {
|
||||
|
||||
{/* 备注 */}
|
||||
<div className="form-group">
|
||||
<label htmlFor="remarks" className="form-label">备注</label>
|
||||
<label htmlFor="remark" className="form-label">备注</label>
|
||||
<textarea
|
||||
id="remarks"
|
||||
name="remarks"
|
||||
id="remark"
|
||||
name="remark"
|
||||
className="form-textarea"
|
||||
defaultValue={config?.remarks || ''}
|
||||
defaultValue={config?.remark || ''}
|
||||
rows={2}
|
||||
placeholder="请输入配置备注信息"
|
||||
/>
|
||||
@@ -658,12 +554,6 @@ export default function ConfigNew() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{actionData?.errors?.general && (
|
||||
<div className="form-row">
|
||||
<div className="error-message general-error">{actionData.errors.general}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
</Form>
|
||||
</Card>
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Outlet } from "react-router-dom";
|
||||
import {type MetaFunction} from "@remix-run/node";
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return [
|
||||
{title: "文档类型列表 - 中国烟草AI合同及卷宗审核系统"},
|
||||
{name: "document-types", content: "文档类型列表,新增,修改"}
|
||||
]
|
||||
}
|
||||
|
||||
export const handle = {
|
||||
breadcrumb: "文档类型列表"
|
||||
}
|
||||
|
||||
/**
|
||||
* 文档类型列表路由布局
|
||||
*/
|
||||
export default function DocumentTypesLayout() {
|
||||
return (
|
||||
<Outlet />
|
||||
)
|
||||
}
|
||||
+538
-352
File diff suppressed because it is too large
Load Diff
+121
-140
@@ -1,11 +1,13 @@
|
||||
import { MetaFunction } from "@remix-run/node";
|
||||
import { useSearchParams, useNavigate } from "@remix-run/react";
|
||||
import { MetaFunction, json, type LoaderFunctionArgs } from "@remix-run/node";
|
||||
import { useSearchParams, useNavigate, useLoaderData } from "@remix-run/react";
|
||||
import { useState } 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";
|
||||
|
||||
// 定义提示词模板类型
|
||||
export interface PromptTemplate {
|
||||
@@ -33,123 +35,61 @@ export const meta: MetaFunction = () => {
|
||||
];
|
||||
};
|
||||
|
||||
|
||||
// 模拟数据
|
||||
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": "文档类型" })
|
||||
}
|
||||
];
|
||||
// 定义加载器返回数据类型
|
||||
interface LoaderData {
|
||||
templates: PromptTemplateUI[];
|
||||
total: number;
|
||||
pageSize: number;
|
||||
currentPage: number;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
// 数据加载器
|
||||
export async function loader() {
|
||||
export async function loader({ request }: LoaderFunctionArgs) {
|
||||
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 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);
|
||||
|
||||
// 使用模拟数据
|
||||
const templates = MOCK_TEMPLATES;
|
||||
console.log('加载提示词模板参数:', { name, type, status, page, pageSize });
|
||||
|
||||
return Response.json({
|
||||
templates,
|
||||
total: templates.length,
|
||||
pageSize: 10,
|
||||
currentPage: 1
|
||||
// 从 API 获取数据
|
||||
const result = await getPromptTemplates({
|
||||
name,
|
||||
type,
|
||||
status,
|
||||
page,
|
||||
pageSize
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
console.error('获取提示词模板失败:', result.error);
|
||||
return json<LoaderData>(
|
||||
{
|
||||
templates: [],
|
||||
total: 0,
|
||||
pageSize,
|
||||
currentPage: page,
|
||||
error: result.error
|
||||
},
|
||||
{ status: result.status || 500 }
|
||||
);
|
||||
}
|
||||
|
||||
console.log(`成功加载${result.data?.templates.length || 0}条提示词模板数据`);
|
||||
|
||||
return json<LoaderData>({
|
||||
templates: result.data?.templates || [],
|
||||
total: result.data?.total || 0,
|
||||
pageSize,
|
||||
currentPage: page
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("加载提示词模板失败:", error);
|
||||
return Response.json(
|
||||
return json<LoaderData>(
|
||||
{
|
||||
templates: [],
|
||||
total: 0,
|
||||
@@ -166,6 +106,8 @@ export async function loader() {
|
||||
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 handleNameSearch = (value: string) => {
|
||||
@@ -210,6 +152,12 @@ export default function PromptsIndex() {
|
||||
setSearchParams(new URLSearchParams());
|
||||
};
|
||||
|
||||
// 处理搜索按钮点击
|
||||
// const handleSearch = () => {
|
||||
// // 搜索已经由 URL 参数变化触发,这里不需要额外操作
|
||||
// console.log('搜索参数:', Object.fromEntries(searchParams.entries()));
|
||||
// };
|
||||
|
||||
// 查看模板详情
|
||||
const handleViewTemplate = (id: string) => {
|
||||
navigate(`/prompts/new?id=${id}&mode=view`);
|
||||
@@ -228,23 +176,48 @@ export default function PromptsIndex() {
|
||||
};
|
||||
|
||||
// 删除模板
|
||||
const handleDeleteTemplate = (id: string) => {
|
||||
const handleDeleteTemplate = async (id: string) => {
|
||||
if (confirm('确定要删除该模板吗?删除后无法恢复。')) {
|
||||
// 实际应该调用API删除数据
|
||||
console.log('删除模板ID:', id);
|
||||
alert('删除成功!');
|
||||
// 刷新页面
|
||||
window.location.reload();
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const result = await deletePromptTemplate(id);
|
||||
if (result.error) {
|
||||
alert(`删除失败: ${result.error}`);
|
||||
} else {
|
||||
alert('删除成功!');
|
||||
// 刷新页面
|
||||
window.location.reload();
|
||||
}
|
||||
} catch (error) {
|
||||
alert(`删除失败: ${error instanceof Error ? error.message : '未知错误'}`);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 处理分页
|
||||
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: "300px",
|
||||
render: (_: unknown, record: PromptTemplate) => (
|
||||
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>
|
||||
@@ -255,7 +228,7 @@ export default function PromptsIndex() {
|
||||
title: "类型",
|
||||
key: "template_type",
|
||||
width: "100px",
|
||||
render: (_: unknown, record: PromptTemplate) => {
|
||||
render: (_: unknown, record: PromptTemplateUI) => {
|
||||
let typeText = '';
|
||||
let typeClass = '';
|
||||
|
||||
@@ -284,7 +257,7 @@ export default function PromptsIndex() {
|
||||
{
|
||||
title: "描述",
|
||||
key: "description",
|
||||
render: (_: unknown, record: PromptTemplate) => (
|
||||
render: (_: unknown, record: PromptTemplateUI) => (
|
||||
<div className="text-secondary text-sm truncate max-w-xs" title={record.description}>
|
||||
{record.description}
|
||||
</div>
|
||||
@@ -294,13 +267,13 @@ export default function PromptsIndex() {
|
||||
title: "版本",
|
||||
key: "version",
|
||||
width: "80px",
|
||||
render: (_: unknown, record: PromptTemplate) => record.version
|
||||
render: (_: unknown, record: PromptTemplateUI) => record.version
|
||||
},
|
||||
{
|
||||
title: "状态",
|
||||
key: "status",
|
||||
width: "80px",
|
||||
render: (_: unknown, record: PromptTemplate) => {
|
||||
render: (_: unknown, record: PromptTemplateUI) => {
|
||||
let statusText = '';
|
||||
let statusClass = '';
|
||||
|
||||
@@ -326,14 +299,15 @@ export default function PromptsIndex() {
|
||||
title: "创建者",
|
||||
key: "created_by",
|
||||
width: "100px",
|
||||
render: (_: unknown, record: PromptTemplate) => record.created_by
|
||||
render: (_: unknown, record: PromptTemplateUI) => (
|
||||
<span className="text-secondary">用户 {record.created_by}</span>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
key: "operation",
|
||||
width: "150px",
|
||||
// align: "center",
|
||||
render: (_: unknown, record: PromptTemplate) => (
|
||||
render: (_: unknown, record: PromptTemplateUI) => (
|
||||
<div>
|
||||
{record.status === 'system' ? (
|
||||
<>
|
||||
@@ -361,6 +335,7 @@ export default function PromptsIndex() {
|
||||
<button
|
||||
className="operation-btn text-error"
|
||||
onClick={() => handleDeleteTemplate(record.id)}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<i className="ri-delete-bin-line"></i> 删除
|
||||
</button>
|
||||
@@ -400,12 +375,13 @@ export default function PromptsIndex() {
|
||||
>
|
||||
重置
|
||||
</Button>
|
||||
<Button
|
||||
{/* <Button
|
||||
type="primary"
|
||||
icon="ri-search-line"
|
||||
onClick={handleSearch}
|
||||
>
|
||||
搜索
|
||||
</Button>
|
||||
</Button> */}
|
||||
</>
|
||||
}
|
||||
noActionDivider={true}
|
||||
@@ -424,7 +400,6 @@ export default function PromptsIndex() {
|
||||
name="type"
|
||||
value={searchParams.get('type') || ''}
|
||||
options={[
|
||||
// { value: "", label: "全部" },
|
||||
{ value: "Extraction", label: "抽取(Extraction)" },
|
||||
{ value: "Evaluation", label: "评估(Evaluation)" },
|
||||
{ value: "Summary", label: "摘要(Summary)" },
|
||||
@@ -439,7 +414,6 @@ export default function PromptsIndex() {
|
||||
name="status"
|
||||
value={searchParams.get('status') || ''}
|
||||
options={[
|
||||
// { value: "", label: "全部" },
|
||||
{ value: "active", label: "启用" },
|
||||
{ value: "inactive", label: "停用" },
|
||||
{ value: "system", label: "系统预设" }
|
||||
@@ -449,27 +423,34 @@ export default function PromptsIndex() {
|
||||
/>
|
||||
</FilterPanel>
|
||||
|
||||
{/* 错误信息 */}
|
||||
{error && (
|
||||
<div className="error-alert mb-4 p-4 bg-red-50 text-red-700 rounded-md">
|
||||
<i className="ri-error-warning-line mr-2"></i> {error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 数据表格 */}
|
||||
<Card bodyClassName="px-4 py-4">
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={MOCK_TEMPLATES}
|
||||
dataSource={templates}
|
||||
rowKey="id"
|
||||
emptyText="暂无提示词模板数据"
|
||||
loading={isLoading}
|
||||
/>
|
||||
|
||||
{/* 分页 */}
|
||||
<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}
|
||||
/>
|
||||
currentPage={currentPage}
|
||||
total={total}
|
||||
pageSize={pageSize}
|
||||
onChange={handlePageChange}
|
||||
onPageSizeChange={handlePageSizeChange}
|
||||
showTotal={true}
|
||||
showPageSizeChanger={true}
|
||||
pageSizeOptions={[10, 20, 30, 50]}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
|
||||
+214
-214
@@ -1,9 +1,9 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { MetaFunction, LoaderFunctionArgs, ActionFunctionArgs } from "@remix-run/node";
|
||||
import { Link, useLoaderData, useSubmit, useNavigation } from "@remix-run/react";
|
||||
import { MetaFunction, LoaderFunctionArgs, ActionFunctionArgs, json, redirect } from "@remix-run/node";
|
||||
import { Link, useLoaderData, useNavigation, useActionData, Form } from "@remix-run/react";
|
||||
import { Button } from "~/components/ui/Button";
|
||||
import { PromptTemplate } from "./prompts._index";
|
||||
import newStyles from "~/styles/pages/prompts_new.css?url";
|
||||
import { getPromptTemplate, createPromptTemplate, updatePromptTemplate, type PromptTemplateUI } from "~/api/prompts/prompts";
|
||||
|
||||
// 样式链接
|
||||
export function links() {
|
||||
@@ -32,111 +32,34 @@ export const handle = {
|
||||
};
|
||||
|
||||
interface LoaderData {
|
||||
template: PromptTemplate;
|
||||
template: PromptTemplateUI | null;
|
||||
mode: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
// 从模拟数据中获取模板
|
||||
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: "抽取采购合同中乙方的资质信息",
|
||||
// 定义本地表单数据接口
|
||||
interface FormDataState extends Omit<PromptTemplateUI, 'variables'> {
|
||||
variables: string; // 在表单状态中我们保存变量为 JSON 字符串
|
||||
}
|
||||
|
||||
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);
|
||||
};
|
||||
interface ActionData {
|
||||
success?: boolean;
|
||||
errors?: {
|
||||
template_name?: string;
|
||||
template_type?: string;
|
||||
template_content?: string;
|
||||
general?: string;
|
||||
};
|
||||
formData?: {
|
||||
template_name: string;
|
||||
template_type: "Extraction" | "Evaluation" | "Summary" | "Common";
|
||||
description: string;
|
||||
template_content: string;
|
||||
variables: string;
|
||||
status: "active" | "inactive" | "system";
|
||||
version: string;
|
||||
};
|
||||
}
|
||||
|
||||
// 加载函数
|
||||
export async function loader({ request }: LoaderFunctionArgs) {
|
||||
@@ -149,25 +72,27 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
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();
|
||||
// 从API获取数据
|
||||
const result = await getPromptTemplate(id);
|
||||
|
||||
// 使用模拟数据
|
||||
template = getTemplateById(id);
|
||||
if (result.error) {
|
||||
console.error('获取提示词模板失败:', result.error);
|
||||
throw new Error(result.error);
|
||||
}
|
||||
|
||||
template = result.data || null;
|
||||
if (!template) {
|
||||
throw new Error(`未找到ID为${id}的模板`);
|
||||
}
|
||||
}
|
||||
|
||||
return Response.json({
|
||||
return json<LoaderData>({
|
||||
template,
|
||||
mode
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("加载提示词模板失败:", error);
|
||||
return Response.json(
|
||||
return json<LoaderData>(
|
||||
{
|
||||
template: null,
|
||||
mode: "create",
|
||||
@@ -180,69 +105,102 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
|
||||
// Action函数 - 处理表单提交
|
||||
export async function action({ request }: ActionFunctionArgs) {
|
||||
const formData = await request.formData();
|
||||
const id = formData.get("id") as string;
|
||||
const template_name = formData.get("template_name") as string;
|
||||
const template_type = formData.get("template_type") as "Extraction" | "Evaluation" | "Summary" | "Common";
|
||||
const description = formData.get("description") as string;
|
||||
const template_content = formData.get("template_content") as string;
|
||||
const variables = formData.get("variables") as string;
|
||||
const status = formData.get("status") as string;
|
||||
const version = formData.get("version") as string;
|
||||
|
||||
const errors: ActionData["errors"] = {};
|
||||
|
||||
// 表单验证
|
||||
if (!template_name || template_name.trim() === "") {
|
||||
errors.template_name = "模板名称不能为空";
|
||||
}
|
||||
|
||||
if (!template_type) {
|
||||
errors.template_type = "请选择模板类型";
|
||||
}
|
||||
|
||||
if (!template_content || template_content.trim() === "") {
|
||||
errors.template_content = "模板内容不能为空";
|
||||
}
|
||||
|
||||
if (Object.keys(errors).length > 0) {
|
||||
return json({ errors });
|
||||
}
|
||||
|
||||
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()
|
||||
// 准备变量数据
|
||||
let variablesData: Record<string, string> = {};
|
||||
try {
|
||||
if (variables) {
|
||||
variablesData = JSON.parse(variables);
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('解析变量JSON失败:', e);
|
||||
}
|
||||
|
||||
// 准备API数据
|
||||
const apiTemplate: Partial<PromptTemplateUI> = {
|
||||
template_name,
|
||||
template_type,
|
||||
description,
|
||||
template_content,
|
||||
variables: variablesData,
|
||||
status: status === "active" ? "active" : "inactive",
|
||||
version: version || "v1.0"
|
||||
};
|
||||
|
||||
let result;
|
||||
if (id) {
|
||||
// 更新模板
|
||||
result = await updatePromptTemplate(id, apiTemplate);
|
||||
} else {
|
||||
// 创建模板
|
||||
result = await createPromptTemplate(apiTemplate);
|
||||
}
|
||||
|
||||
if (result.error) {
|
||||
return json({
|
||||
errors: { general: result.error },
|
||||
formData: {
|
||||
template_name,
|
||||
template_type,
|
||||
description,
|
||||
template_content,
|
||||
variables,
|
||||
status,
|
||||
version
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return redirect("/prompts");
|
||||
} catch (error) {
|
||||
console.error("保存提示词模板失败:", error);
|
||||
return Response.json(
|
||||
{
|
||||
success: false,
|
||||
message: error instanceof Error ? error.message : "保存提示词模板失败"
|
||||
return json({
|
||||
errors: {
|
||||
general: error instanceof Error ? error.message : "保存提示词模板失败"
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
formData: {
|
||||
template_name,
|
||||
template_type,
|
||||
description,
|
||||
template_content,
|
||||
variables,
|
||||
status,
|
||||
version
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 提取变量函数
|
||||
// 提取变量函数 例如:{var1} {var2} {var3}
|
||||
const extractVariables = (content: string) => {
|
||||
const regex = /{([^{}]+)}/g;
|
||||
const variables: Record<string, string> = {};
|
||||
@@ -260,13 +218,13 @@ const extractVariables = (content: string) => {
|
||||
|
||||
// 页面组件
|
||||
export default function PromptsNew() {
|
||||
const { template, mode } = useLoaderData<typeof loader>();
|
||||
const submit = useSubmit();
|
||||
const { template, mode, error } = useLoaderData<typeof loader>();
|
||||
const actionData = useActionData<ActionData>();
|
||||
const navigation = useNavigation();
|
||||
const isSubmitting = navigation.state === "submitting";
|
||||
|
||||
// 表单状态
|
||||
const [formData, setFormData] = useState<Partial<PromptTemplate>>({
|
||||
const [formData, setFormData] = useState<FormDataState>({
|
||||
id: "",
|
||||
template_name: "",
|
||||
template_type: "Common",
|
||||
@@ -274,7 +232,10 @@ export default function PromptsNew() {
|
||||
version: "v1.0",
|
||||
status: "active",
|
||||
template_content: "",
|
||||
variables: "{}"
|
||||
created_by: 1,
|
||||
variables: "{}",
|
||||
created_at: "",
|
||||
updated_at: ""
|
||||
});
|
||||
|
||||
// 模式状态
|
||||
@@ -282,43 +243,57 @@ export default function PromptsNew() {
|
||||
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) {
|
||||
if (actionData?.formData) {
|
||||
// 如果有保存失败的表单数据,使用它
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
...actionData.formData,
|
||||
variables: actionData.formData?.variables || "{}",
|
||||
status: actionData.formData?.status || "active"
|
||||
}));
|
||||
} else if (template) {
|
||||
// 否则使用模板数据
|
||||
const variablesJson = typeof template.variables === 'string'
|
||||
? template.variables
|
||||
: JSON.stringify(template.variables);
|
||||
|
||||
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
|
||||
version: mode === "clone" ? "v1.0" : template.version,
|
||||
variables: variablesJson
|
||||
};
|
||||
|
||||
setFormData(newFormData);
|
||||
|
||||
try {
|
||||
// 解析模板变量
|
||||
const vars = JSON.parse(template.variables);
|
||||
const vars = typeof template.variables === 'string'
|
||||
? JSON.parse(template.variables)
|
||||
: 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") {
|
||||
@@ -328,7 +303,7 @@ export default function PromptsNew() {
|
||||
} else {
|
||||
setPageTitle("新增提示词模板");
|
||||
}
|
||||
}, [template, mode]);
|
||||
}, [template, mode, actionData?.formData]);
|
||||
|
||||
// 处理输入变化
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
|
||||
@@ -337,16 +312,18 @@ export default function PromptsNew() {
|
||||
// 如果是模板内容,检测变量
|
||||
if (name === "template_content") {
|
||||
const vars = extractVariables(value);
|
||||
// console.log("检测到的变量:",vars);
|
||||
setDetectedVariables(vars);
|
||||
|
||||
// 更新变量JSON
|
||||
const varsJson = JSON.stringify(
|
||||
Object.keys(vars).reduce((acc, key) => {
|
||||
acc[key] = exampleValues[key] || key;
|
||||
acc[key] = exampleValues[key] || "";
|
||||
return acc;
|
||||
}, {} as Record<string, string>)
|
||||
);
|
||||
|
||||
// console.log("更新变量JSON:",varsJson);
|
||||
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
[name]: value,
|
||||
@@ -362,11 +339,19 @@ export default function PromptsNew() {
|
||||
|
||||
// 处理状态切换
|
||||
const handleStatusToggle = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
// console.log("状态切换前:", formData.status);
|
||||
const status = e.target.checked ? "active" : "inactive";
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
status
|
||||
}));
|
||||
// console.log("新状态值:", status);
|
||||
|
||||
// 直接更新formData状态
|
||||
setFormData(prev => {
|
||||
const newState = {
|
||||
...prev,
|
||||
status: status as "active" | "inactive" | "system"
|
||||
};
|
||||
// console.log("状态更新后:", newState.status);
|
||||
return newState;
|
||||
});
|
||||
};
|
||||
|
||||
// 处理示例值变更
|
||||
@@ -383,7 +368,7 @@ export default function PromptsNew() {
|
||||
|
||||
// 替换变量
|
||||
Object.entries(detectedVariables).forEach(([key]) => {
|
||||
const exampleValue = exampleValues[key] || `[${key}]`;
|
||||
const exampleValue = exampleValues[key] || ``;
|
||||
const regex = new RegExp(`{${key}}`, 'g');
|
||||
content = content.replace(regex, exampleValue);
|
||||
});
|
||||
@@ -395,7 +380,7 @@ export default function PromptsNew() {
|
||||
useEffect(() => {
|
||||
const varsJson = JSON.stringify(
|
||||
Object.keys(detectedVariables).reduce((acc, key) => {
|
||||
acc[key] = exampleValues[key] || key;
|
||||
acc[key] = exampleValues[key] || "";
|
||||
return acc;
|
||||
}, {} as Record<string, string>)
|
||||
);
|
||||
@@ -405,23 +390,6 @@ export default function PromptsNew() {
|
||||
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">
|
||||
@@ -439,7 +407,7 @@ export default function PromptsNew() {
|
||||
type="primary"
|
||||
icon="ri-save-line"
|
||||
disabled={isSubmitting}
|
||||
onClick={() => document.getElementById("template-form")?.dispatchEvent(new Event("submit", { cancelable: true, bubbles: true }))}
|
||||
form="template-form"
|
||||
>
|
||||
{isSubmitting ? "保存中..." : "保存"}
|
||||
</Button>
|
||||
@@ -447,6 +415,21 @@ export default function PromptsNew() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 错误信息 */}
|
||||
{error && (
|
||||
<div className="alert alert-error mb-4">
|
||||
<i className="ri-error-warning-line"></i>
|
||||
<div>{error}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{actionData?.errors?.general && (
|
||||
<div className="alert alert-error mb-4">
|
||||
<i className="ri-error-warning-line"></i>
|
||||
<div>{actionData.errors.general}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 查看模式提示 */}
|
||||
{isViewMode && (
|
||||
<div className="alert alert-info">
|
||||
@@ -458,7 +441,7 @@ export default function PromptsNew() {
|
||||
)}
|
||||
|
||||
{/* 模板表单 */}
|
||||
<form id="template-form" method="post" onSubmit={handleSubmit}>
|
||||
<Form id="template-form" method="post">
|
||||
{/* 模板ID - 隐藏字段 */}
|
||||
<input type="hidden" name="id" value={formData.id || ''} />
|
||||
|
||||
@@ -474,7 +457,7 @@ export default function PromptsNew() {
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
className={`form-input py-1 ${isViewMode ? 'read-only-field' : ''}`}
|
||||
className={`form-input py-1 ${isViewMode ? 'read-only-field' : ''} ${actionData?.errors?.template_name ? 'input-error' : ''}`}
|
||||
id="template-name"
|
||||
name="template_name"
|
||||
placeholder="请输入模板名称"
|
||||
@@ -483,6 +466,9 @@ export default function PromptsNew() {
|
||||
readOnly={isViewMode}
|
||||
required
|
||||
/>
|
||||
{actionData?.errors?.template_name && (
|
||||
<div className="error-message">{actionData.errors.template_name}</div>
|
||||
)}
|
||||
<div className="help-text text-xs">建议使用"文档类型-功能"的命名方式</div>
|
||||
</div>
|
||||
|
||||
@@ -492,7 +478,7 @@ export default function PromptsNew() {
|
||||
模板类型 <span className="text-error">*</span>
|
||||
</label>
|
||||
<select
|
||||
className={`form-select py-1 ${isViewMode ? 'read-only-field' : ''}`}
|
||||
className={`form-select py-1 ${isViewMode ? 'read-only-field' : ''} ${actionData?.errors?.template_type ? 'input-error' : ''}`}
|
||||
id="template-type"
|
||||
name="template_type"
|
||||
value={formData.template_type || ''}
|
||||
@@ -506,6 +492,9 @@ export default function PromptsNew() {
|
||||
<option value="Evaluation">评估(Evaluation) - 对文档内容进行评估</option>
|
||||
<option value="Summary">摘要(Summary) - 生成文档内容摘要</option>
|
||||
</select>
|
||||
{actionData?.errors?.template_type && (
|
||||
<div className="error-message">{actionData.errors.template_type}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 模板描述 */}
|
||||
@@ -535,15 +524,22 @@ export default function PromptsNew() {
|
||||
<input
|
||||
type="checkbox"
|
||||
id="status-toggle"
|
||||
name="status"
|
||||
checked={formData.status === 'active'}
|
||||
onChange={handleStatusToggle}
|
||||
disabled={isViewMode}
|
||||
/>
|
||||
{/* 移除name属性,只使用隐藏字段传递状态值 */}
|
||||
<span className="slider"></span>
|
||||
</label>
|
||||
<span id="status-text">{formData.status === 'active' ? '启用' : '停用'}</span>
|
||||
|
||||
</div>
|
||||
{/* 使用隐藏字段传递状态值 */}
|
||||
<input
|
||||
type="hidden"
|
||||
name="status"
|
||||
value={formData.status}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 模板版本 */}
|
||||
@@ -584,7 +580,7 @@ export default function PromptsNew() {
|
||||
模板内容 <span className="text-error">*</span>
|
||||
</label>
|
||||
<textarea
|
||||
className={`form-code-editor w-full ${isViewMode ? 'read-only-field' : ''}`}
|
||||
className={`form-code-editor w-full ${isViewMode ? 'read-only-field' : ''} ${actionData?.errors?.template_content ? 'input-error' : ''}`}
|
||||
id="template-content"
|
||||
name="template_content"
|
||||
placeholder="在此输入提示词模板内容..."
|
||||
@@ -594,6 +590,9 @@ export default function PromptsNew() {
|
||||
rows={15}
|
||||
required
|
||||
></textarea>
|
||||
{actionData?.errors?.template_content && (
|
||||
<div className="error-message">{actionData.errors.template_content}</div>
|
||||
)}
|
||||
<div className="help-text">提示词模板是AI完成特定任务的指令,请清晰描述任务需求和输出格式</div>
|
||||
</div>
|
||||
|
||||
@@ -642,6 +641,7 @@ export default function PromptsNew() {
|
||||
className="form-input"
|
||||
value={varName}
|
||||
readOnly
|
||||
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
@@ -703,7 +703,7 @@ export default function PromptsNew() {
|
||||
</Link>
|
||||
{!isViewMode && (
|
||||
<button
|
||||
type="submit"
|
||||
form="template-form"
|
||||
className="ant-btn ant-btn-primary"
|
||||
disabled={isSubmitting}
|
||||
id="save-btn-bottom"
|
||||
@@ -713,7 +713,7 @@ export default function PromptsNew() {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user