优化评查详情,新增信息提示框组件

This commit is contained in:
2025-04-23 20:48:32 +08:00
parent ee36ce2620
commit be99fdec79
15 changed files with 1399 additions and 757 deletions
+441 -180
View File
@@ -1,11 +1,12 @@
import { json, redirect, type ActionFunctionArgs, type LoaderFunctionArgs, type MetaFunction } from "@remix-run/node";
import { 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 { useEffect, useState, useRef } 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 { 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";
export const links = () => [
{ rel: "stylesheet", href: configNewStyles }
@@ -39,6 +40,7 @@ export const EXTENDED_ENVIRONMENT_LABELS: Record<string, string> = {
[ExtendedConfigEnvironment.COMMON]: '通用'
};
// 新增配置表单数据
interface ConfigData {
id: number;
name: string;
@@ -49,41 +51,16 @@ interface ConfigData {
remark?: string;
}
// 加载器数据类型
interface LoaderData {
config?: ConfigData;
isEdit: boolean;
types: string[];
environments: string[];
error?: 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?: {
@@ -93,81 +70,7 @@ interface ActionData {
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: "保存配置失败,请稍后重试"
}
});
}
values?: Record<string, string>;
}
// 配置模板常量
@@ -205,29 +108,212 @@ const CONFIG_TEMPLATES = {
}
};
export async function loader({ request }: LoaderFunctionArgs) {
const url = new URL(request.url);
const id = url.searchParams.get("id");
let config: ConfigData | undefined = undefined;
try {
// 获取配置选项
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 || [],
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;
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);
if (response.error) {
throw new Error(response.error);
}
} else {
// 创建配置
const response = await createConfig(configData);
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 { config, isEdit, types, environments } = useLoaderData<typeof loader>();
const data = useLoaderData<typeof loader>();
const actionData = useActionData<typeof action>();
const navigation = useNavigation();
const isSubmitting = navigation.state === "submitting";
const formRef = useRef<HTMLFormElement>(null);
const [jsonError, setJsonError] = useState<string | null>(null);
const [configDataValue, setConfigDataValue] = useState("");
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 [selectedModule, setSelectedModule] = useState<string>("");
const [selectedEnvironment, setSelectedEnvironment] = useState<string>("");
// 在 ConfigNew 组件中添加状态来跟踪当前选中的模板
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) {
setConfigDataValue(JSON.stringify(config.config, null, 2));
setSelectedModule(config.type);
setSelectedEnvironment(config.environment);
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
@@ -246,46 +332,213 @@ export default function ConfigNew() {
}, null, 2));
}, [config]);
// 处理JSON数据变更
// 验证表单字段
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.value;
setConfigDataValue(value);
const { value } = e.target;
if (value.trim() === "") {
setJsonError(null);
return;
}
setFormValues(prev => ({
...prev,
config: value
}));
try {
JSON.parse(value);
setJsonError(null);
} catch (error) {
if (error instanceof Error) {
setJsonError(`配置数据必须是有效的JSON格式: ${error.message}`);
} else {
setJsonError("配置数据必须是有效的JSON格式");
}
}
// 标记字段为已触摸
setTouchedFields(prev => ({
...prev,
config: true
}));
// 实时验证
const error = validateField('config', value);
setFormErrors(prev => ({
...prev,
config: error
}));
};
// 格式化JSON
const handleFormatJson = () => {
if (configDataValue.trim() === "") return;
if (formValues.config.trim() === "") return;
try {
const parsed = JSON.parse(configDataValue);
setConfigDataValue(JSON.stringify(parsed, null, 2));
setJsonError(null);
const parsed = JSON.parse(formValues.config);
const formatted = JSON.stringify(parsed, null, 2);
setFormValues(prev => ({
...prev,
config: formatted
}));
setFormErrors(prev => ({
...prev,
config: ""
}));
} catch (error) {
if (error instanceof Error) {
setJsonError(`当前不是有效的JSON,无法格式化: ${error.message}`);
} else {
setJsonError("当前不是有效的JSON,无法格式化");
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">
@@ -302,14 +555,20 @@ export default function ConfigNew() {
</div>
</div>
{actionData?.errors?.general && (
{formErrors.general && (
<div className="mb-4 w-full">
<div className="error-message general-error">{actionData.errors.general}</div>
<div className="error-message general-error">{formErrors.general}</div>
</div>
)}
<Card className="config-form-card">
<Form method="post" id="configForm" className="config-form">
<Form
method="post"
id="configForm"
className="config-form"
ref={formRef}
onSubmit={handleBeforeSubmit}
>
{config?.id && <input type="hidden" name="id" value={config.id} />}
{/* 配置名称和状态 */}
@@ -320,13 +579,14 @@ export default function ConfigNew() {
type="text"
id="name"
name="name"
className={`form-input ${actionData?.errors?.name ? 'input-error' : ''}`}
defaultValue={config?.name || ''}
className={`form-input ${touchedFields.name && formErrors.name ? 'input-error' : ''}`}
value={formValues.name}
onChange={handleInputChange}
placeholder="请输入配置名称,如database_connection"
required
/>
{actionData?.errors?.name && (
<div className="error-message">{actionData.errors.name}</div>
{touchedFields.name && formErrors.name && (
<div className="error-message">{formErrors.name}</div>
)}
<div className="form-help">
使使线
@@ -343,7 +603,8 @@ export default function ConfigNew() {
name="is_active"
value="true"
className="form-checkbox"
defaultChecked={config?.is_active !== false}
checked={formValues.is_active}
onChange={handleActiveChange}
/>
<label htmlFor="is_active" className="form-checkbox-label">
@@ -362,27 +623,28 @@ export default function ConfigNew() {
<input
type="hidden"
name="type"
value={selectedModule}
value={formValues.type}
/>
<input
type="text"
id="typeDisplay"
className={`form-input ${actionData?.errors?.type ? 'input-error' : ''}`}
value={selectedModule}
onChange={(e) => setSelectedModule(e.target.value)}
className={`form-input ${touchedFields.type && formErrors.type ? 'input-error' : ''}`}
value={formValues.type}
onChange={handleInputChange}
name="type"
placeholder="请输入或选择所属模块"
required
/>
{actionData?.errors?.type && (
<div className="error-message">{actionData.errors.type}</div>
{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 ${selectedModule === type ? 'active' : ''}`}
onClick={() => setSelectedModule(type)}
className={`tag-button ${formValues.type === type ? 'active' : ''}`}
onClick={() => handleModuleSelect(type)}
>
{type}
</button>
@@ -399,27 +661,28 @@ export default function ConfigNew() {
<input
type="hidden"
name="environment"
value={selectedEnvironment}
value={formValues.environment}
/>
<input
type="text"
id="environmentDisplay"
className={`form-input ${actionData?.errors?.environment ? 'input-error' : ''}`}
value={selectedEnvironment}
onChange={(e) => setSelectedEnvironment(e.target.value)}
className={`form-input ${touchedFields.environment && formErrors.environment ? 'input-error' : ''}`}
value={formValues.environment}
onChange={handleInputChange}
name="environment"
placeholder="请输入或选择环境"
required
/>
{actionData?.errors?.environment && (
<div className="error-message">{actionData.errors.environment}</div>
{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 ${selectedEnvironment === env ? 'active' : ''}`}
onClick={() => setSelectedEnvironment(env)}
className={`tag-button ${formValues.environment === env ? 'active' : ''}`}
onClick={() => handleEnvironmentSelect(env)}
>
{env}
</button>
@@ -439,8 +702,8 @@ export default function ConfigNew() {
<textarea
id="config"
name="config"
className={`json-editor ${(actionData?.errors?.config || jsonError) ? 'input-error' : ''}`}
value={configDataValue}
className={`json-editor ${touchedFields.config && formErrors.config ? 'input-error' : ''}`}
value={formValues.config}
onChange={handleConfigDataChange}
required
placeholder='请输入JSON格式的配置数据'
@@ -457,8 +720,8 @@ export default function ConfigNew() {
<i className="ri-braces-line mr-1"></i> JSON
</Button>
</div>
{(actionData?.errors?.config || jsonError) && (
<div className="error-message">{actionData?.errors?.config || jsonError}</div>
{touchedFields.config && formErrors.config && (
<div className="error-message">{formErrors.config}</div>
)}
</div>
@@ -498,8 +761,7 @@ export default function ConfigNew() {
className={`${selectedTemplate === 'database' ? 'border-[#00684a] text-[#00684a] focus:ring-0' : ''}`}
onClick={(e) => {
e.preventDefault();
setExampleJsonValue(JSON.stringify(CONFIG_TEMPLATES.database, null, 2));
setSelectedTemplate('database');
handleTemplateSelect('database');
}}
>
@@ -510,8 +772,7 @@ export default function ConfigNew() {
className={`${selectedTemplate === 'file' ? 'border-[#00684a] text-[#00684a] focus:ring-0' : ''}`}
onClick={(e) => {
e.preventDefault();
setExampleJsonValue(JSON.stringify(CONFIG_TEMPLATES.file, null, 2));
setSelectedTemplate('file');
handleTemplateSelect('file');
}}
>
@@ -522,8 +783,7 @@ export default function ConfigNew() {
className={`${selectedTemplate === 'ai' ? 'border-[#00684a] text-[#00684a] focus:ring-0' : ''}`}
onClick={(e) => {
e.preventDefault();
setExampleJsonValue(JSON.stringify(CONFIG_TEMPLATES.ai, null, 2));
setSelectedTemplate('ai');
handleTemplateSelect('ai');
}}
>
AI服务配置
@@ -545,7 +805,8 @@ export default function ConfigNew() {
id="remark"
name="remark"
className="form-textarea"
defaultValue={config?.remark || ''}
value={formValues.remark}
onChange={handleInputChange}
rows={2}
placeholder="请输入配置备注信息"
/>