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

563 lines
18 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 { getConfigOptions, getConfigDetail, createConfig, updateConfig } from "~/api/system_setting/config-lists";
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: 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) {
const url = new URL(request.url);
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) {
// 获取配置详情
const detailResponse = await getConfigDetail(id);
if (detailResponse.error) {
throw new Error(detailResponse.error);
}
config = detailResponse.data;
}
return Response.json({
config,
isEdit: !!config,
types: optionsResponse.data?.types || [],
environments: optionsResponse.data?.environments || []
});
}
interface ActionData {
success?: boolean;
errors?: {
name?: string;
type?: string;
environment?: string;
config?: string;
general?: string;
};
}
export async function action({ request }: ActionFunctionArgs) {
const formData = await request.formData();
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 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 (!name || name.trim() === "") {
errors.name = "配置名称不能为空";
}
if (!type) {
errors.type = "请选择所属模块";
}
if (!environment) {
errors.environment = "请选择环境";
}
if (!config || config.trim() === "") {
errors.config = "配置数据不能为空";
} else {
try {
JSON.parse(config);
} catch (e) {
errors.config = "配置数据必须是有效的JSON格式";
}
}
if (Object.keys(errors).length > 0) {
return Response.json({ errors });
}
try {
const configData = {
name,
type,
environment,
config: JSON.parse(config),
is_active,
remark
};
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 Response.json({
success: false,
errors: {
general: "保存配置失败,请稍后重试"
}
});
}
}
// 配置模板常量
const CONFIG_TEMPLATES = {
database: {
host: "localhost",
port: 5432,
database: "mydb",
username: "admin",
password: "******",
pool: {
min: 2,
max: 10
}
},
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, types, environments } = 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>("");
// 在 ConfigNew 组件中添加状态来跟踪当前选中的模板
const [selectedTemplate, setSelectedTemplate] = useState<keyof typeof CONFIG_TEMPLATES | null>(null);
useEffect(() => {
// 初始化配置数据
if (config) {
setConfigDataValue(JSON.stringify(config.config, null, 2));
setSelectedModule(config.type);
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,无法格式化");
}
}
};
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>
{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">
{config?.id && <input type="hidden" name="id" value={config.id} />}
{/* 配置名称和状态 */}
<div className="form-row">
<div className="form-group">
<label htmlFor="name" className="form-label required"></label>
<input
type="text"
id="name"
name="name"
className={`form-input ${actionData?.errors?.name ? 'input-error' : ''}`}
defaultValue={config?.name || ''}
placeholder="请输入配置名称,如database_connection"
required
/>
{actionData?.errors?.name && (
<div className="error-message">{actionData.errors.name}</div>
)}
<div className="form-help">
使使线
</div>
</div>
<div className="form-group">
<label htmlFor="is_active" className="form-label"></label>
<div className="mt-2">
<div className="flex items-center">
<input
type="checkbox"
id="is_active"
name="is_active"
value="true"
className="form-checkbox"
defaultChecked={config?.is_active !== false}
/>
<label htmlFor="is_active" className="form-checkbox-label">
</label>
</div>
<div className="form-help">
</div>
</div>
</div>
</div>
{/* 所属模块 */}
<div className="form-group">
<label htmlFor="type" className="form-label required"></label>
<input
type="hidden"
name="type"
value={selectedModule}
/>
<input
type="text"
id="typeDisplay"
className={`form-input ${actionData?.errors?.type ? 'input-error' : ''}`}
value={selectedModule}
onChange={(e) => setSelectedModule(e.target.value)}
placeholder="请输入或选择所属模块"
required
/>
{actionData?.errors?.type && (
<div className="error-message">{actionData.errors.type}</div>
)}
<div className="tag-buttons mt-2">
{types.map((type: string) => (
<button
key={type}
type="button"
className={`tag-button ${selectedModule === type ? 'active' : ''}`}
onClick={() => setSelectedModule(type)}
>
{type}
</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}
onChange={(e) => setSelectedEnvironment(e.target.value)}
placeholder="请输入或选择环境"
required
/>
{actionData?.errors?.environment && (
<div className="error-message">{actionData.errors.environment}</div>
)}
<div className="tag-buttons mt-2">
{environments.map((env: string) => (
<button
key={env}
type="button"
className={`tag-button ${selectedEnvironment === env ? 'active' : ''}`}
onClick={() => setSelectedEnvironment(env)}
>
{env}
</button>
))}
</div>
<div className="form-help">
使使
</div>
</div>
{/* 配置数据 */}
<div className="form-group">
<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="config"
name="config"
className={`json-editor ${(actionData?.errors?.config || jsonError) ? 'input-error' : ''}`}
value={configDataValue}
onChange={handleConfigDataChange}
required
placeholder='请输入JSON格式的配置数据'
/>
<div className="editor-actions">
<Button
type="default"
size="small"
onClick={(e) => {
e.preventDefault();
handleFormatJson();
}}
>
<i className="ri-braces-line mr-1"></i> JSON
</Button>
</div>
{(actionData?.errors?.config || jsonError) && (
<div className="error-message">{actionData?.errors?.config || 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">
<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"
size="small"
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"
size="small"
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"
size="small"
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>
</div>
</div>
</div>
</div>
</div>
<div className="form-help">
JSON格式的配置数据
</div>
</div>
{/* 备注 */}
<div className="form-group">
<label htmlFor="remark" className="form-label"></label>
<textarea
id="remark"
name="remark"
className="form-textarea"
defaultValue={config?.remark || ''}
rows={2}
placeholder="请输入配置备注信息"
/>
<div className="form-help">
</div>
</div>
</Form>
</Card>
</div>
);
}