673 lines
21 KiB
TypeScript
673 lines
21 KiB
TypeScript
import { json, redirect, type ActionFunctionArgs, type LoaderFunctionArgs, type MetaFunction } from "@remix-run/node";
|
||
import { Form, useActionData, useLoaderData, useNavigation } from "@remix-run/react";
|
||
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 configNewStyles from "~/styles/pages/config-lists_new.css?url";
|
||
|
||
export const links = () => [
|
||
{ rel: "stylesheet", href: configNewStyles }
|
||
];
|
||
|
||
export const handle = {
|
||
breadcrumb: (data:LoaderData) => {
|
||
return data.isEdit ? "编辑配置" : "新增配置";
|
||
}
|
||
};
|
||
|
||
export const meta: MetaFunction = () => {
|
||
return [
|
||
{ title: "新增配置 - 中国烟草AI合同及卷宗审核系统" },
|
||
{ name: "description", content: "新增或编辑系统配置项" },
|
||
{ name: "keywords", content: "配置管理,系统配置,新增配置,编辑配置" }
|
||
];
|
||
};
|
||
|
||
// 扩展环境枚举,添加"通用"选项
|
||
export enum ExtendedConfigEnvironment {
|
||
DEV = 'dev',
|
||
TEST = 'test',
|
||
PROD = 'prod',
|
||
COMMON = 'common'
|
||
}
|
||
|
||
// 扩展环境标签映射
|
||
export const EXTENDED_ENVIRONMENT_LABELS: Record<string, string> = {
|
||
...ENVIRONMENT_LABELS,
|
||
[ExtendedConfigEnvironment.COMMON]: '通用'
|
||
};
|
||
|
||
interface ConfigData {
|
||
id: string;
|
||
configName: string;
|
||
module: ConfigModule;
|
||
environment: string; // 使用扩展的环境类型
|
||
isActive: boolean;
|
||
configData: string; // JSON字符串
|
||
remarks?: string; // 添加备注字段
|
||
}
|
||
|
||
interface LoaderData {
|
||
config?: ConfigData;
|
||
isEdit: boolean;
|
||
}
|
||
|
||
export async function loader({ request }: LoaderFunctionArgs) {
|
||
const url = new URL(request.url);
|
||
const id = url.searchParams.get("id");
|
||
let config: ConfigData | undefined = undefined;
|
||
|
||
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
|
||
}
|
||
}
|
||
|
||
return Response.json({
|
||
config,
|
||
isEdit: !!config
|
||
});
|
||
}
|
||
|
||
|
||
interface ActionData {
|
||
success?: boolean;
|
||
errors?: {
|
||
configName?: string;
|
||
module?: string;
|
||
environment?: string;
|
||
configData?: 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 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 errors: ActionData["errors"] = {};
|
||
|
||
// 表单验证
|
||
if (!configName || configName.trim() === "") {
|
||
errors.configName = "配置名称不能为空";
|
||
}
|
||
|
||
if (!module) {
|
||
errors.module = "请选择所属模块";
|
||
}
|
||
|
||
if (!environment) {
|
||
errors.environment = "请选择环境";
|
||
}
|
||
|
||
if (!configData || configData.trim() === "") {
|
||
errors.configData = "配置数据不能为空";
|
||
} else {
|
||
try {
|
||
JSON.parse(configData);
|
||
} catch (e) {
|
||
errors.configData = "配置数据必须是有效的JSON格式";
|
||
}
|
||
}
|
||
|
||
if (Object.keys(errors).length > 0) {
|
||
return json<ActionData>({ errors });
|
||
}
|
||
|
||
try {
|
||
// 实际应用中,应调用API保存数据
|
||
console.log("保存配置:", { configId, configName, module, environment, configData, isActive, remarks });
|
||
|
||
// 模拟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}`);
|
||
// }
|
||
|
||
// 保存成功后重定向到列表页
|
||
return redirect("/config-lists");
|
||
} catch (error) {
|
||
console.error("保存配置失败:", error);
|
||
return json<ActionData>({
|
||
success: false,
|
||
errors: {
|
||
general: "保存配置失败,请稍后重试"
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
// JSON模板数据
|
||
const JSON_TEMPLATES = {
|
||
database: {
|
||
database: {
|
||
host: "localhost",
|
||
port: 5432,
|
||
username: "db_user",
|
||
password: "******",
|
||
name: "app_database",
|
||
pool_size: 10,
|
||
timeout: 5000,
|
||
ssl: false
|
||
}
|
||
},
|
||
file: {
|
||
storage: {
|
||
type: "local", // or "s3", "oss"
|
||
path: "/data/uploads",
|
||
allowed_types: ["pdf", "docx", "jpg", "png"],
|
||
max_size: 10485760, // 10MB
|
||
backup_enabled: true
|
||
}
|
||
},
|
||
ai: {
|
||
ai_service: {
|
||
provider: "openai",
|
||
api_key: "sk-******",
|
||
model: "gpt-4",
|
||
max_tokens: 2000,
|
||
temperature: 0.7,
|
||
timeout: 30000,
|
||
rate_limit: 10 // requests per minute
|
||
}
|
||
}
|
||
};
|
||
|
||
export default function ConfigNew() {
|
||
const { config, isEdit } = useLoaderData<typeof loader>();
|
||
|
||
const actionData = useActionData<typeof action>();
|
||
const navigation = useNavigation();
|
||
const isSubmitting = navigation.state === "submitting";
|
||
|
||
const [jsonError, setJsonError] = useState<string | null>(null);
|
||
const [configDataValue, setConfigDataValue] = useState("");
|
||
const [exampleJsonValue, setExampleJsonValue] = useState("");
|
||
|
||
// 标签选择状态
|
||
const [selectedModule, setSelectedModule] = useState<string>("");
|
||
const [selectedEnvironment, setSelectedEnvironment] = useState<string>("");
|
||
|
||
useEffect(() => {
|
||
// 初始化配置数据
|
||
if (config?.configData) {
|
||
setConfigDataValue(config.configData);
|
||
}
|
||
|
||
// 初始化模块和环境的选中状态
|
||
if (config?.module) {
|
||
setSelectedModule(config.module);
|
||
}
|
||
|
||
if (config?.environment) {
|
||
setSelectedEnvironment(config.environment);
|
||
}
|
||
|
||
// 初始化示例JSON
|
||
setExampleJsonValue(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));
|
||
|
||
}, [config]);
|
||
|
||
// 处理JSON数据变更
|
||
const handleConfigDataChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||
const value = e.target.value;
|
||
setConfigDataValue(value);
|
||
|
||
if (value.trim() === "") {
|
||
setJsonError(null);
|
||
return;
|
||
}
|
||
|
||
try {
|
||
JSON.parse(value);
|
||
setJsonError(null);
|
||
} catch (error) {
|
||
if (error instanceof Error) {
|
||
setJsonError(`配置数据必须是有效的JSON格式: ${error.message}`);
|
||
} else {
|
||
setJsonError("配置数据必须是有效的JSON格式");
|
||
}
|
||
}
|
||
};
|
||
|
||
// 格式化JSON
|
||
const handleFormatJson = () => {
|
||
if (configDataValue.trim() === "") return;
|
||
|
||
try {
|
||
const parsed = JSON.parse(configDataValue);
|
||
setConfigDataValue(JSON.stringify(parsed, null, 2));
|
||
setJsonError(null);
|
||
} catch (error) {
|
||
if (error instanceof Error) {
|
||
setJsonError(`当前不是有效的JSON,无法格式化: ${error.message}`);
|
||
} else {
|
||
setJsonError("当前不是有效的JSON,无法格式化");
|
||
}
|
||
}
|
||
};
|
||
|
||
// 加载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">
|
||
<span className="block text-xl font-medium">{isEdit ? "编辑系统配置" : "新增系统配置"}</span>
|
||
<div className="form-actions">
|
||
<Button type="default" to="/config-lists">
|
||
<i className="ri-arrow-left-line mr-1"></i>
|
||
返回
|
||
</Button>
|
||
<Button type="primary" disabled={isSubmitting} form="configForm">
|
||
<i className="ri-save-line mr-1"></i>
|
||
{isSubmitting ? '保存中...' : '保存'}
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
|
||
<Card className="config-form-card">
|
||
<Form method="post" id="configForm" className="config-form">
|
||
{config?.id && <input type="hidden" name="id" value={config.id} />}
|
||
|
||
{/* 配置名称和状态 */}
|
||
<div className="form-row">
|
||
<div className="form-group">
|
||
<label htmlFor="configName" className="form-label required">配置名称</label>
|
||
<input
|
||
type="text"
|
||
id="configName"
|
||
name="configName"
|
||
className={`form-input ${actionData?.errors?.configName ? 'input-error' : ''}`}
|
||
defaultValue={config?.configName || ''}
|
||
placeholder="请输入配置名称,如database_connection"
|
||
required
|
||
/>
|
||
{actionData?.errors?.configName && (
|
||
<div className="error-message">{actionData.errors.configName}</div>
|
||
)}
|
||
<div className="form-help">
|
||
唯一标识符,配置名称应使用英文,推荐使用下划线命名方式
|
||
</div>
|
||
</div>
|
||
|
||
<div className="form-group">
|
||
<label htmlFor="isActive" className="form-label">状态</label>
|
||
<div className="mt-2">
|
||
<div className="flex items-center">
|
||
<input
|
||
type="checkbox"
|
||
id="isActive"
|
||
name="isActive"
|
||
value="true"
|
||
className="form-checkbox"
|
||
defaultChecked={config?.isActive !== false}
|
||
/>
|
||
<label htmlFor="isActive" className="form-checkbox-label">
|
||
启用此配置
|
||
</label>
|
||
</div>
|
||
<div className="form-help">
|
||
禁用配置后,系统将不会读取此配置
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 所属模块 */}
|
||
<div className="form-group">
|
||
<label htmlFor="module" className="form-label required">所属模块</label>
|
||
<input
|
||
type="hidden"
|
||
name="module"
|
||
value={selectedModule}
|
||
/>
|
||
<input
|
||
type="text"
|
||
id="moduleDisplay"
|
||
className={`form-input ${actionData?.errors?.module ? 'input-error' : ''}`}
|
||
value={selectedModule ? MODULE_LABELS[selectedModule as ConfigModule] || selectedModule : ''}
|
||
placeholder="请输入或选择所属模块"
|
||
readOnly
|
||
required
|
||
/>
|
||
{actionData?.errors?.module && (
|
||
<div className="error-message">{actionData.errors.module}</div>
|
||
)}
|
||
<div className="tag-buttons mt-2">
|
||
{Object.entries(MODULE_LABELS).map(([value, label]) => (
|
||
<button
|
||
key={value}
|
||
type="button"
|
||
className={`tag-button ${selectedModule === value ? 'active' : ''}`}
|
||
onClick={() => handleModuleTagClick(value)}
|
||
>
|
||
{label}
|
||
</button>
|
||
))}
|
||
</div>
|
||
<div className="form-help">
|
||
将配置按功能模块进行分类,便于管理和查找
|
||
</div>
|
||
</div>
|
||
|
||
{/* 环境 */}
|
||
<div className="form-group">
|
||
<label htmlFor="environment" className="form-label required">环境</label>
|
||
<input
|
||
type="hidden"
|
||
name="environment"
|
||
value={selectedEnvironment}
|
||
/>
|
||
<input
|
||
type="text"
|
||
id="environmentDisplay"
|
||
className={`form-input ${actionData?.errors?.environment ? 'input-error' : ''}`}
|
||
value={selectedEnvironment ? EXTENDED_ENVIRONMENT_LABELS[selectedEnvironment] || selectedEnvironment : ''}
|
||
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]) => (
|
||
<button
|
||
key={value}
|
||
type="button"
|
||
className={`tag-button ${selectedEnvironment === value ? 'active' : ''}`}
|
||
onClick={() => handleEnvironmentTagClick(value)}
|
||
>
|
||
{label}
|
||
</button>
|
||
))}
|
||
</div>
|
||
<div className="form-help">
|
||
不同环境可以使用相同的配置名称,系统会自动识别当前环境并使用对应配置
|
||
</div>
|
||
</div>
|
||
|
||
{/* 配置数据 */}
|
||
<div className="form-group">
|
||
<label htmlFor="configData" 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' : ''}`}
|
||
value={configDataValue}
|
||
onChange={handleConfigDataChange}
|
||
required
|
||
placeholder='请输入JSON格式的配置数据'
|
||
/>
|
||
<div className="editor-actions">
|
||
<Button
|
||
type="default"
|
||
size="small"
|
||
onClick={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>
|
||
)}
|
||
</div>
|
||
|
||
{/* 右侧示例区 */}
|
||
<div className="h-full">
|
||
<div className="example-card">
|
||
<div className="example-header">
|
||
<div className="example-title">配置示例</div>
|
||
</div>
|
||
<div className="example-content">
|
||
<pre
|
||
className="example-pre"
|
||
dangerouslySetInnerHTML={{ __html: renderJsonWithSyntaxHighlight(exampleJsonValue) }}
|
||
/>
|
||
</div>
|
||
<div className="example-footer">
|
||
<div className="text-sm font-medium mb-2">常用配置模板:</div>
|
||
<div className="flex flex-wrap gap-2">
|
||
<Button
|
||
type="default"
|
||
size="small"
|
||
onClick={() => handleLoadTemplate('database')}
|
||
>
|
||
数据库配置
|
||
</Button>
|
||
<Button
|
||
type="default"
|
||
size="small"
|
||
onClick={() => handleLoadTemplate('file')}
|
||
>
|
||
文件存储配置
|
||
</Button>
|
||
<Button
|
||
type="default"
|
||
size="small"
|
||
onClick={() => handleLoadTemplate('ai')}
|
||
>
|
||
AI服务配置
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div className="form-help">
|
||
请输入JSON格式的配置数据,支持嵌套结构
|
||
</div>
|
||
</div>
|
||
|
||
{/* 备注 */}
|
||
<div className="form-group">
|
||
<label htmlFor="remarks" className="form-label">备注</label>
|
||
<textarea
|
||
id="remarks"
|
||
name="remarks"
|
||
className="form-textarea"
|
||
defaultValue={config?.remarks || ''}
|
||
rows={2}
|
||
placeholder="请输入配置备注信息"
|
||
/>
|
||
<div className="form-help">
|
||
可选填项,用于描述配置的用途、注意事项等
|
||
</div>
|
||
</div>
|
||
|
||
{actionData?.errors?.general && (
|
||
<div className="form-row">
|
||
<div className="error-message general-error">{actionData.errors.general}</div>
|
||
</div>
|
||
)}
|
||
|
||
|
||
</Form>
|
||
</Card>
|
||
</div>
|
||
);
|
||
}
|