603 lines
18 KiB
TypeScript
603 lines
18 KiB
TypeScript
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 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>
|
||
);
|
||
}
|