Files
leaudit-platform-frontend/app/routes/config-lists.new.tsx
T

673 lines
21 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.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>
);
}