新增配置列表和配置新增页面

This commit is contained in:
2025-03-28 15:41:11 +08:00
parent 540618b8ca
commit afadd79fe8
16 changed files with 1608 additions and 473 deletions
+605
View File
@@ -0,0 +1,605 @@
import { json, type MetaFunction, type LoaderFunctionArgs, type ActionFunctionArgs } from "@remix-run/node";
import { useLoaderData, useSearchParams, useSubmit, Link } from "@remix-run/react";
import { useState } from "react";
import { Button } from "~/components/ui/Button";
import { Card } from "~/components/ui/Card";
import { FilterPanel, FilterSelect, SearchFilter } from "~/components/ui/FilterPanel";
import { Pagination } from "~/components/ui/Pagination";
import { Table } from "~/components/ui/Table";
import { Tag } from "~/components/ui/Tag";
import configListsStyles from "~/styles/pages/config-lists_index.css?url";
export const links = () => [
{ rel: "stylesheet", href: configListsStyles }
];
// export const handle = {
// breadcrumb: "系统配置管理"
// };
export const meta: MetaFunction = () => {
return [
{ title: "系统配置管理 - 中国烟草AI合同及卷宗审核系统" },
{ name: "description", content: "管理系统配置,包括数据库、文件存储、AI引擎等配置项" },
{ name: "keywords", content: "系统配置,配置管理,中国烟草,参数设置" }
];
};
// 配置环境枚举
export enum ConfigEnvironment {
DEV = 'dev',
TEST = 'test',
PROD = 'prod'
}
// 配置模块枚举
export enum ConfigModule {
SYSTEM = 'system',
AUTH = 'auth',
FILE = 'file',
AI = 'ai',
NOTIFICATION = 'notification'
}
// 环境标签映射
export const ENVIRONMENT_LABELS: Record<ConfigEnvironment, string> = {
[ConfigEnvironment.DEV]: '开发环境',
[ConfigEnvironment.TEST]: '测试环境',
[ConfigEnvironment.PROD]: '生产环境'
};
// 模块标签映射
export const MODULE_LABELS: Record<ConfigModule, string> = {
[ConfigModule.SYSTEM]: '系统',
[ConfigModule.AUTH]: '认证',
[ConfigModule.FILE]: '文件',
[ConfigModule.AI]: 'AI配置',
[ConfigModule.NOTIFICATION]: '通知'
};
// 配置数据类型
interface ConfigDataType {
[key: string]: string | number | boolean | string[] | ConfigDataType | ConfigDataType[];
}
// 配置项模型
interface ConfigItem {
id: string;
configName: string;
module: ConfigModule;
environment: ConfigEnvironment;
isActive: boolean;
configData: ConfigDataType;
createdAt: string;
updatedAt: string;
}
interface LoaderData {
configs: ConfigItem[];
totalCount: number;
currentPage: number;
pageSize: number;
totalPages: number;
}
export async function loader({ request }: LoaderFunctionArgs) {
const url = new URL(request.url);
const configName = url.searchParams.get("configName") || "";
const module = url.searchParams.get("module") || "";
const environment = url.searchParams.get("environment") || "";
const isActive = url.searchParams.get("isActive") || "";
const currentPage = parseInt(url.searchParams.get("page") || "1", 10);
const pageSize = parseInt(url.searchParams.get("pageSize") || "10", 10);
try {
// 模拟数据,实际项目中应从API获取
const mockConfigs: ConfigItem[] = [
{
id: "1",
configName: "database_connection",
module: ConfigModule.SYSTEM,
environment: ConfigEnvironment.PROD,
isActive: true,
configData: {
database: {
host: "db.cluster.com",
port: 5432,
pool_size: 20,
ssl: true
},
cache: {
ttl: 3600,
max_entries: 1000
},
feature_flags: ["new_ui", "analytics_v2"]
},
createdAt: "2023-07-10 10:15:23",
updatedAt: "2023-07-15 14:30:26"
},
{
id: "2",
configName: "text_extraction_ai",
module: ConfigModule.AI,
environment: ConfigEnvironment.TEST,
isActive: true,
configData: {
model: "gpt-4",
parameters: {
temperature: 0.7,
max_tokens: 2000
},
api_key: "sk-**********",
timeout: 30
},
createdAt: "2023-07-12 08:45:12",
updatedAt: "2023-07-14 09:15:33"
},
{
id: "3",
configName: "notification_service",
module: ConfigModule.NOTIFICATION,
environment: ConfigEnvironment.DEV,
isActive: false,
configData: {
email: {
smtp_server: "smtp.example.com",
port: 587,
use_tls: true,
sender: "noreply@example.com"
},
sms: {
provider: "aliyun",
region: "cn-hangzhou",
sign_name: "AI审核系统"
}
},
createdAt: "2023-07-05 13:20:45",
updatedAt: "2023-07-10 16:45:19"
},
{
id: "4",
configName: "file_storage",
module: ConfigModule.FILE,
environment: ConfigEnvironment.PROD,
isActive: true,
configData: {
type: "oss",
region: "cn-shanghai",
bucket: "contracts-ai-review",
access_control: "private",
lifecycle_rules: [
{
prefix: "temp/",
ttl_days: 7
}
]
},
createdAt: "2023-06-28 09:30:18",
updatedAt: "2023-07-08 11:22:07"
}
];
// 过滤数据
let filteredConfigs = [...mockConfigs];
if (configName) {
filteredConfigs = filteredConfigs.filter(config =>
config.configName.toLowerCase().includes(configName.toLowerCase())
);
}
if (module) {
filteredConfigs = filteredConfigs.filter(config => config.module === module);
}
if (environment) {
filteredConfigs = filteredConfigs.filter(config => config.environment === environment);
}
if (isActive) {
const activeValue = isActive === 'true';
filteredConfigs = filteredConfigs.filter(config => config.isActive === activeValue);
}
// 计算分页信息
const totalCount = filteredConfigs.length;
const totalPages = Math.ceil(totalCount / pageSize);
// 分页截取
const startIndex = (currentPage - 1) * pageSize;
const endIndex = startIndex + pageSize;
const paginatedConfigs = filteredConfigs.slice(startIndex, endIndex);
return json<LoaderData>({
configs: paginatedConfigs,
totalCount,
currentPage,
pageSize,
totalPages
}, {
headers: {
"Cache-Control": "max-age=60, s-maxage=180"
}
});
} catch (error) {
console.error('加载配置列表失败:', error);
throw new Response('加载配置列表失败', { status: 500 });
}
}
export async function action({ request }: ActionFunctionArgs) {
const formData = await request.formData();
const _action = formData.get('_action');
const configId = formData.get('configId');
if (!configId) {
return json({ success: false, error: "缺少配置ID" }, { status: 400 });
}
try {
if (_action === 'toggleStatus') {
const isActive = formData.get('isActive') === 'true';
const newStatus = !isActive;
// 实际项目中应调用API更新状态
console.log(`切换配置 ${configId} 状态为: ${newStatus}`);
// 模拟API调用
// const response = await fetch(`/api/configs/${configId}/status`, {
// method: 'PATCH',
// headers: {
// 'Content-Type': 'application/json',
// },
// body: JSON.stringify({ isActive: newStatus }),
// });
// if (!response.ok) {
// throw new Error(`状态切换失败: ${response.status}`);
// }
return json({ success: true, newStatus });
}
return json({ success: false, error: "未知操作" }, { status: 400 });
} catch (error) {
console.error('操作配置失败:', error);
return json({ success: false, error: "操作失败" }, { status: 500 });
}
}
export function ErrorBoundary() {
return (
<div className="error-container p-6">
<h1 className="text-xl font-bold text-red-500 mb-4"></h1>
<p className="mb-4"></p>
<Button type="primary" to="/"></Button>
</div>
);
}
export default function ConfigListsIndex() {
const { configs, totalCount, currentPage, pageSize } = useLoaderData<typeof loader>();
const [searchParams, setSearchParams] = useSearchParams();
const submit = useSubmit();
const [showDetailModal, setShowDetailModal] = useState(false);
const [selectedConfig, setSelectedConfig] = useState<ConfigItem | null>(null);
const handleFilterChange = (e: React.ChangeEvent<HTMLSelectElement | HTMLInputElement>) => {
const { name, value } = e.target;
const newParams = new URLSearchParams(searchParams);
if (value) {
newParams.set(name, value);
} else {
newParams.delete(name);
}
// 切换筛选条件时,重置到第一页
newParams.set('page', '1');
setSearchParams(newParams);
};
const handleConfigNameSearch = (value: string) => {
const newParams = new URLSearchParams(searchParams);
if (value) {
newParams.set('configName', value);
} else {
newParams.delete('configName');
}
// 搜索时,重置到第一页
newParams.set('page', '1');
setSearchParams(newParams);
};
const handleToggleStatus = (config: ConfigItem) => {
if (window.confirm(`确定要${config.isActive ? '禁用' : '启用'}该配置吗?`)) {
const formData = new FormData();
formData.append('_action', 'toggleStatus');
formData.append('configId', config.id);
formData.append('isActive', String(config.isActive));
submit(formData, { method: 'post' });
}
};
const handleViewDetail = (config: ConfigItem) => {
setSelectedConfig(config);
setShowDetailModal(true);
};
const handlePageChange = (page: number) => {
const newParams = new URLSearchParams(searchParams);
newParams.set('page', page.toString());
setSearchParams(newParams);
};
const handlePageSizeChange = (size: number) => {
const newParams = new URLSearchParams(searchParams);
newParams.set('pageSize', size.toString());
newParams.set('page', '1'); // 更改每页条数时,重置到第一页
setSearchParams(newParams);
};
// 处理重置筛选
const handleReset = () => {
setSearchParams(new URLSearchParams());
};
// 关闭详情模态框
const closeDetailModal = () => {
setShowDetailModal(false);
setSelectedConfig(null);
};
// 定义表格列配置
const columns = [
{
title: "配置名称",
dataIndex: "configName" as keyof ConfigItem,
key: "configName",
width: "20%"
},
{
title: "所属模块",
key: "module",
width: "10%",
render: (_: unknown, record: ConfigItem) => MODULE_LABELS[record.module]
},
{
title: "环境",
key: "environment",
width: "15%",
render: (_: unknown, record: ConfigItem) => {
const envClass = `env-tag env-tag-${record.environment}`;
return (
<span className={envClass}>
{ENVIRONMENT_LABELS[record.environment]}
</span>
);
}
},
{
title: "状态",
key: "isActive",
width: "15%",
render: (_: unknown, record: ConfigItem) => (
<Tag color={record.isActive ? 'green' : 'red'}>
{record.isActive ? '已启用' : '已禁用'}
</Tag>
)
},
{
title: "最后更新时间",
dataIndex: "updatedAt" as keyof ConfigItem,
key: "updatedAt",
width: "15%"
},
{
title: "操作",
key: "operation",
width: "25%",
render: (_: unknown, record: ConfigItem) => (
<div className="operations-cell">
<button
type="button"
className="operation-btn"
onClick={() => handleViewDetail(record)}
>
<i className="ri-eye-line"></i>
</button>
<Link
to={`/config-lists/new?id=${record.id}`}
type="button"
className="operation-btn"
>
<i className="ri-edit-line"></i>
</Link>
<button
type="button"
className={`operation-btn ${record.isActive ? '!text-[--color-warning]' : '!text-[--color-success]'}`}
onClick={() => handleToggleStatus(record)}
>
<i className={record.isActive ? `ri-stop-circle-line` : `ri-play-circle-line`}></i>
{record.isActive ? '禁用' : '启用'}
</button>
</div>
)
}
];
// 生成环境选项
const environmentOptions = Object.entries(ENVIRONMENT_LABELS).map(([value, label]) => ({
value,
label
}));
// 生成模块选项
const moduleOptions = Object.entries(MODULE_LABELS).map(([value, label]) => ({
value,
label
}));
return (
<div className="config-lists">
{/* 页面头部 */}
<div className="flex justify-between items-center mb-4">
<h2 className="text-xl font-medium"></h2>
<Button type="primary" icon="ri-add-line" to="/config-lists/new">
</Button>
</div>
{/* 搜索区域 */}
<FilterPanel
className="mb-4"
actions={
<>
<Button type="default" icon="ri-refresh-line" onClick={handleReset} className="mr-2">
</Button>
<Button type="primary" icon="ri-search-line">
</Button>
</>
}
noActionDivider={true}
>
<SearchFilter
label="配置名称"
placeholder="请输入配置名称"
value={searchParams.get('configName') || ''}
onSearch={handleConfigNameSearch}
className="flex-1 min-w-[200px]"
instantSearch={true}
/>
<FilterSelect
label="所属模块"
name="module"
value={searchParams.get('module') || ''}
options={[{ value: '', label: '全部' }, ...moduleOptions]}
onChange={handleFilterChange}
className="flex-1 min-w-[200px]"
/>
<FilterSelect
label="环境"
name="environment"
value={searchParams.get('environment') || ''}
options={[{ value: '', label: '全部' }, ...environmentOptions]}
onChange={handleFilterChange}
className="flex-1 min-w-[200px]"
/>
<FilterSelect
label="状态"
name="isActive"
value={searchParams.get('isActive') || ''}
options={[
{ value: '', label: '全部' },
{ value: 'true', label: '已启用' },
{ value: 'false', label: '已禁用' }
]}
onChange={handleFilterChange}
className="flex-1 min-w-[200px]"
/>
</FilterPanel>
{/* 表格区域 */}
<Card>
<Table
columns={columns}
dataSource={configs}
rowKey="id"
emptyText="暂无配置数据"
className="config-table"
/>
{/* 分页区域 */}
{totalCount > 0 && (
<Pagination
currentPage={currentPage}
total={totalCount}
pageSize={pageSize}
onChange={handlePageChange}
onPageSizeChange={handlePageSizeChange}
showTotal={true}
showPageSizeChanger={true}
pageSizeOptions={[10, 20, 30, 50]}
/>
)}
</Card>
{/* 配置详情模态框 */}
{showDetailModal && selectedConfig && (
<div className="fixed inset-0 bg-black bg-opacity-30 flex items-center justify-center z-50">
<div className="bg-white rounded-lg p-6 max-w-3xl w-full">
<div className="flex justify-between items-center mb-4">
<h3 className="text-lg font-medium"></h3>
<button className="text-gray-500 hover:text-gray-700" onClick={closeDetailModal}>
<i className="ri-close-line text-xl"></i>
</button>
</div>
<div className="config-detail-content">
<div className="config-detail-item">
<div className="config-detail-label"></div>
<div className="config-detail-value">{selectedConfig.configName}</div>
</div>
<div className="config-detail-item">
<div className="config-detail-label"></div>
<div className="config-detail-value">{MODULE_LABELS[selectedConfig.module]}</div>
</div>
<div className="config-detail-item">
<div className="config-detail-label"></div>
<div className="config-detail-value">
<span className={`env-tag env-tag-${selectedConfig.environment}`}>
{ENVIRONMENT_LABELS[selectedConfig.environment]}
</span>
</div>
</div>
<div className="config-detail-item">
<div className="config-detail-label"></div>
<div className="config-detail-value">
<Tag color={selectedConfig.isActive ? 'green' : 'red'}>
{selectedConfig.isActive ? '已启用' : '已禁用'}
</Tag>
</div>
</div>
<div className="config-detail-item">
<div className="config-detail-label"></div>
<pre className="config-detail-code">
{JSON.stringify(selectedConfig.configData, null, 2)}
</pre>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="config-detail-item">
<div className="config-detail-label"></div>
<div className="config-detail-value">{selectedConfig.createdAt}</div>
</div>
<div className="config-detail-item">
<div className="config-detail-label"></div>
<div className="config-detail-value">{selectedConfig.updatedAt}</div>
</div>
</div>
</div>
<div className="flex justify-end mt-6">
<Button type="default" onClick={closeDetailModal}></Button>
</div>
</div>
</div>
)}
</div>
);
}