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

829 lines
25 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 { redirect, type ActionFunctionArgs, type LoaderFunctionArgs, type MetaFunction } from "@remix-run/node";
import { Form, useActionData, useLoaderData, useNavigation } from "@remix-run/react";
import { useEffect, useState, useRef } from "react";
import { Button } from "~/components/ui/Button";
import { Card } from "~/components/ui/Card";
import { 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";
import { toastService } from "~/components/ui/Toast";
import { getUserSession } from "~/api/login/auth.server";
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[];
error?: string;
}
// 新增配置表单数据
interface ActionData {
success?: boolean;
errors?: {
name?: string;
type?: string;
environment?: string;
config?: string;
general?: string;
};
values?: Record<string, string>;
}
// 配置模板常量
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 async function loader({ request }: LoaderFunctionArgs) {
const url = new URL(request.url);
const id = url.searchParams.get("id");
let config: ConfigData | undefined = undefined;
// 获取JWT token
const { frontendJWT } = await getUserSession(request);
try {
// 获取配置选项
const optionsResponse = await getConfigOptions(frontendJWT);
if (optionsResponse.error) {
throw new Error(optionsResponse.error);
}
if (id) {
// 获取配置详情
const detailResponse = await getConfigDetail(id, frontendJWT);
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 || [],
error: undefined
});
} catch (error) {
console.error("加载配置数据失败:", error);
return Response.json({
config: undefined,
isEdit: false,
types: [],
environments: [],
error: error instanceof Error ? error.message : "加载配置数据失败"
});
}
}
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;
// 获取JWT token
const { frontendJWT } = await getUserSession(request);
const errors: ActionData["errors"] = {};
// 表单验证
if (!name || name.trim() === "") {
errors.name = "配置名称不能为空";
}else if(/^[a-zA-Z_]+$/.test(name)){
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,
values: Object.fromEntries(formData) as Record<string, string>
});
}
try {
const configData = {
name,
type,
environment,
config: JSON.parse(config),
is_active,
remark
};
if (id) {
// 更新配置
const response = await updateConfig(id, configData, frontendJWT);
if (response.error) {
throw new Error(response.error);
}
} else {
// 创建配置
const response = await createConfig(configData, frontendJWT);
if (response.error) {
throw new Error(response.error);
}
}
// 保存成功,显示成功提示并重定向
toastService.success("保存成功");
return redirect("/config-lists");
} catch (error) {
console.error("保存配置失败:", error);
return Response.json({
success: false,
errors: {
general: error instanceof Error ? error.message : "保存配置失败,请稍后重试"
},
values: Object.fromEntries(formData) as Record<string, string>
});
}
}
export default function ConfigNew() {
const data = useLoaderData<typeof loader>();
const actionData = useActionData<typeof action>();
const navigation = useNavigation();
const isSubmitting = navigation.state === "submitting";
const formRef = useRef<HTMLFormElement>(null);
const { config, isEdit, types, environments, error } = data;
// 表单状态管理
const [formValues, setFormValues] = useState<{
name: string;
type: string;
environment: string;
config: string;
is_active: boolean;
remark: string;
}>({
name: config?.name || "",
type: config?.type || "",
environment: config?.environment || "",
config: config?.config ? JSON.stringify(config.config, null, 2) : "",
is_active: config?.is_active ?? true,
remark: config?.remark || "",
});
// 表单验证错误状态
const [formErrors, setFormErrors] = useState<{
name?: string;
type?: string;
environment?: string;
config?: string;
general?: string;
}>({});
// 字段是否被触摸过(用于确定何时显示错误)
const [touchedFields, setTouchedFields] = useState<{
name: boolean;
type: boolean;
environment: boolean;
config: boolean;
}>({
name: false,
type: false,
environment: false,
config: false
});
// 示例JSON状态
const [exampleJsonValue, setExampleJsonValue] = useState("");
const [selectedTemplate, setSelectedTemplate] = useState<keyof typeof CONFIG_TEMPLATES | null>(null);
// 从 actionData 初始化表单错误
useEffect(() => {
if (actionData?.errors) {
setFormErrors(actionData.errors);
}
// 如果提交后有错误,则将所有字段标记为已触摸
if (actionData?.errors && Object.keys(actionData.errors).length > 0) {
setTouchedFields({
name: true,
type: true,
environment: true,
config: true
});
}
}, [actionData]);
// 根据加载的配置数据初始化表单
useEffect(() => {
if (config) {
setFormValues({
name: config.name,
type: config.type,
environment: config.environment,
config: JSON.stringify(config.config, null, 2),
is_active: config.is_active,
remark: config.remark || ""
});
}
// 初始化示例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]);
// 验证表单字段
const validateField = (field: string, value: string): string => {
switch (field) {
case 'name':
if(value.trim() === ""){
return "配置名称不能为空";
}else if(!/^[a-zA-Z_]+$/.test(value)){
return "配置名称只能包含英文字母和下划线";
}else if(value.length > 100){
return "配置名称不能超过100个字符";
}
return "";
case 'type':
return value.trim() === "" ? "请选择所属模块" : "";
case 'environment':
return value.trim() === "" ? "请选择环境" : "";
case 'config':
if (value.trim() === "") {
return "配置数据不能为空";
} else {
try {
JSON.parse(value);
return "";
} catch (e) {
return "配置数据必须是有效的JSON格式";
}
}
default:
return "";
}
};
// 处理字段改变
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const { name, value } = e.target;
setFormValues(prev => ({
...prev,
[name]: value
}));
// 标记字段为已触摸
if (['name', 'type', 'environment', 'config'].includes(name)) {
setTouchedFields(prev => ({
...prev,
[name]: true
}));
}
// 实时验证
const error = validateField(name, value);
setFormErrors(prev => ({
...prev,
[name]: error
}));
};
// 处理配置数据变更(JSON编辑器)
const handleConfigDataChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
const { value } = e.target;
setFormValues(prev => ({
...prev,
config: value
}));
// 标记字段为已触摸
setTouchedFields(prev => ({
...prev,
config: true
}));
// 实时验证
const error = validateField('config', value);
setFormErrors(prev => ({
...prev,
config: error
}));
};
// 格式化JSON
const handleFormatJson = () => {
if (formValues.config.trim() === "") return;
try {
const parsed = JSON.parse(formValues.config);
const formatted = JSON.stringify(parsed, null, 2);
setFormValues(prev => ({
...prev,
config: formatted
}));
setFormErrors(prev => ({
...prev,
config: ""
}));
} catch (error) {
setFormErrors(prev => ({
...prev,
config: `当前不是有效的JSON,无法格式化: ${error instanceof Error ? error.message : '未知错误'}`
}));
}
};
// 处理模块类型选择
const handleModuleSelect = (moduleType: string) => {
setFormValues(prev => ({
...prev,
type: moduleType
}));
// 标记字段为已触摸
setTouchedFields(prev => ({
...prev,
type: true
}));
// 清除错误
setFormErrors(prev => ({
...prev,
type: ""
}));
};
// 处理环境选择
const handleEnvironmentSelect = (env: string) => {
setFormValues(prev => ({
...prev,
environment: env
}));
// 标记字段为已触摸
setTouchedFields(prev => ({
...prev,
environment: true
}));
// 清除错误
setFormErrors(prev => ({
...prev,
environment: ""
}));
};
// 处理模板选择
const handleTemplateSelect = (templateKey: keyof typeof CONFIG_TEMPLATES) => {
setExampleJsonValue(JSON.stringify(CONFIG_TEMPLATES[templateKey], null, 2));
setSelectedTemplate(templateKey);
};
// 处理启用状态变更
const handleActiveChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setFormValues(prev => ({
...prev,
is_active: e.target.checked
}));
};
// 提交前验证
const handleBeforeSubmit = (e: React.FormEvent) => {
// 标记所有字段为已触摸
setTouchedFields({
name: true,
type: true,
environment: true,
config: true
});
// 验证所有字段
const errors = {
name: validateField('name', formValues.name),
type: validateField('type', formValues.type),
environment: validateField('environment', formValues.environment),
config: validateField('config', formValues.config)
};
setFormErrors(errors);
// 如果有错误,阻止提交
if (errors.name || errors.type || errors.environment || errors.config) {
e.preventDefault();
// 滚动到第一个错误字段
if (formRef.current) {
const firstErrorField = Object.keys(errors).find(key => !!errors[key as keyof typeof errors]);
if (firstErrorField) {
const element = formRef.current.querySelector(`[name="${firstErrorField}"]`);
if (element) {
element.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}
}
}
};
// 如果加载数据时出错,显示错误信息
if (error) {
return (
<div className="error-container p-6">
<h1 className="text-xl font-bold text-red-500 mb-4"></h1>
<p className="mb-4">{error}</p>
<Button type="primary" to="/config-lists"></Button>
</div>
);
}
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>
{formErrors.general && (
<div className="mb-4 w-full">
<div className="error-message general-error">{formErrors.general}</div>
</div>
)}
<Card className="config-form-card">
<Form
method="post"
id="configForm"
className="config-form"
ref={formRef}
onSubmit={handleBeforeSubmit}
>
{config?.id && <input type="hidden" name="id" value={config.id} />}
{/* 配置名称和状态 */}
<div className="form-row">
<div className="form-group">
<label htmlFor="name" className="form-label "> <span className="text-red-500">*</span></label>
<input
type="text"
id="name"
name="name"
className={`form-input ${touchedFields.name && formErrors.name ? 'input-error' : ''}`}
value={formValues.name}
onChange={handleInputChange}
placeholder="请输入配置名称,如database_connection"
/>
{touchedFields.name && formErrors.name && (
<div className="error-message">{formErrors.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"
checked={formValues.is_active}
onChange={handleActiveChange}
/>
<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"> <span className="text-red-500">*</span></label>
<input
type="hidden"
name="type"
value={formValues.type}
/>
<input
type="text"
id="typeDisplay"
className={`form-input ${touchedFields.type && formErrors.type ? 'input-error' : ''}`}
value={formValues.type}
onChange={handleInputChange}
name="type"
placeholder="请输入或选择所属模块"
/>
{touchedFields.type && formErrors.type && (
<div className="error-message">{formErrors.type}</div>
)}
<div className="tag-buttons mt-2">
{types.map((type: string) => (
<button
key={type}
type="button"
className={`tag-button ${formValues.type === type ? 'active' : ''}`}
onClick={() => handleModuleSelect(type)}
>
{type}
</button>
))}
</div>
<div className="form-help">
便
</div>
</div>
{/* 环境 */}
<div className="form-group">
<label htmlFor="environment" className="form-label"> <span className="text-red-500">*</span></label>
<input
type="hidden"
name="environment"
value={formValues.environment}
/>
<input
type="text"
id="environmentDisplay"
className={`form-input ${touchedFields.environment && formErrors.environment ? 'input-error' : ''}`}
value={formValues.environment}
onChange={handleInputChange}
name="environment"
placeholder="请输入或选择环境"
/>
{touchedFields.environment && formErrors.environment && (
<div className="error-message">{formErrors.environment}</div>
)}
<div className="tag-buttons mt-2">
{environments.map((env: string) => (
<button
key={env}
type="button"
className={`tag-button ${formValues.environment === env ? 'active' : ''}`}
onClick={() => handleEnvironmentSelect(env)}
>
{env}
</button>
))}
</div>
<div className="form-help">
使使
</div>
</div>
{/* 配置数据 */}
<div className="form-group">
<label htmlFor="config" className="form-label "> (JSON) <span className="text-red-500">*</span></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 ${touchedFields.config && formErrors.config ? 'input-error' : ''}`}
value={formValues.config}
onChange={handleConfigDataChange}
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>
{touchedFields.config && formErrors.config && (
<div className="error-message">{formErrors.config}</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();
handleTemplateSelect('database');
}}
>
</Button>
<Button
type="default"
size="small"
className={`${selectedTemplate === 'file' ? 'border-[#00684a] text-[#00684a] focus:ring-0' : ''}`}
onClick={(e) => {
e.preventDefault();
handleTemplateSelect('file');
}}
>
</Button>
<Button
type="default"
size="small"
className={`${selectedTemplate === 'ai' ? 'border-[#00684a] text-[#00684a] focus:ring-0' : ''}`}
onClick={(e) => {
e.preventDefault();
handleTemplateSelect('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"
value={formValues.remark}
onChange={handleInputChange}
rows={2}
placeholder="请输入配置备注信息"
/>
<div className="form-help">
</div>
</div>
</Form>
</Card>
</div>
);
}