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

479 lines
15 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, 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 { getConfigLists, getConfigOptions, updateConfigStatus, type ConfigItem } from "~/api/system_setting/config-lists";
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 LoaderData {
configs: ConfigItem[];
totalCount: number;
currentPage: number;
pageSize: number;
totalPages: number;
types: string[];
environments: string[];
}
export async function loader({ request }: LoaderFunctionArgs) {
const url = new URL(request.url);
const name = url.searchParams.get("name") || "";
const type = url.searchParams.get("type") || "";
const environment = url.searchParams.get("environment") || "";
const is_active = url.searchParams.get("is_active") ? url.searchParams.get("is_active") === "true" : undefined;
const currentPage = parseInt(url.searchParams.get("page") || "1", 10);
const pageSize = parseInt(url.searchParams.get("pageSize") || "10", 10);
try {
// 获取配置列表
const configsResponse = await getConfigLists({
name,
type,
environment,
is_active,
page: currentPage,
pageSize
});
if (configsResponse.error || !configsResponse.data) {
throw new Error(configsResponse.error || "获取配置列表失败");
}
// 获取配置选项
const optionsResponse = await getConfigOptions();
if (optionsResponse.error || !optionsResponse.data) {
throw new Error(optionsResponse.error || "获取配置选项失败");
}
return json<LoaderData>({
configs: configsResponse.data,
totalCount: configsResponse.total,
currentPage,
pageSize,
totalPages: Math.ceil(configsResponse.total / pageSize),
types: optionsResponse.data.types,
environments: optionsResponse.data.environments
}, {
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 is_active = formData.get('is_active') === 'true';
const response = await updateConfigStatus(parseInt(configId as string), is_active);
if (!response.success) {
return json({ success: false, error: response.error }, { status: 500 });
}
return json({ success: true });
}
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, types, environments } = 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('name', value);
} else {
newParams.delete('name');
}
// 搜索时,重置到第一页
newParams.set('page', '1');
setSearchParams(newParams);
};
const handleToggleStatus = (config: ConfigItem) => {
if (window.confirm(`确定要${config.is_active ? '禁用' : '启用'}该配置吗?`)) {
const formData = new FormData();
formData.append('_action', 'toggleStatus');
formData.append('configId', config.id.toString());
formData.append('is_active', String(!config.is_active));
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 = () => {
const nameInput = document.querySelector('input[placeholder="请输入配置名称"]') as HTMLInputElement;
const typeSelect = document.querySelector('select[name="type"]') as HTMLInputElement;
const environmentSelect = document.querySelector('select[name="environment"]') as HTMLInputElement;
const statusSelect = document.querySelector('select[name="is_active"]') as HTMLInputElement;
setSearchParams(new URLSearchParams());
if(nameInput) nameInput.value = ''
if(typeSelect) typeSelect.value = ''
if(environmentSelect) environmentSelect.value = ''
if(statusSelect) statusSelect.value = ''
};
// 关闭详情模态框
const closeDetailModal = () => {
setShowDetailModal(false);
setSelectedConfig(null);
};
// 定义表格列配置
const columns = [
{
title: "配置名称",
dataIndex: "name" as keyof ConfigItem,
key: "name",
width: "20%"
},
{
title: "所属模块",
key: "type",
width: "10%",
render: (_: unknown, record: ConfigItem) => record.type
},
{
title: "环境",
key: "environment",
width: "15%",
render: (_: unknown, record: ConfigItem) => {
return (
<span className="env-tag">
{record.environment}
</span>
);
}
},
{
title: "状态",
key: "is_active",
width: "15%",
render: (_: unknown, record: ConfigItem) => (
<Tag color={record.is_active ? 'green' : 'red'}>
{record.is_active ? '已启用' : '已禁用'}
</Tag>
)
},
{
title: "最后更新时间",
dataIndex: "updated_at" as keyof ConfigItem,
key: "updated_at",
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.is_active ? '!text-[--color-warning]' : '!text-[--color-success]'}`}
onClick={() => handleToggleStatus(record)}
>
<i className={record.is_active ? `ri-stop-circle-line` : `ri-play-circle-line`}></i>
{record.is_active ? '禁用' : '启用'}
</button>
</div>
)
}
];
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('name') || ''}
onSearch={handleConfigNameSearch}
className="flex-1 min-w-[200px]"
instantSearch={true}
/>
<FilterSelect
label="所属模块"
name="type"
value={searchParams.get('type') || ''}
options={[ ...types.map(type => ({ value: type, label: type }))]}
onChange={handleFilterChange}
className="flex-1 min-w-[200px]"
/>
<FilterSelect
label="环境"
name="environment"
value={searchParams.get('environment') || ''}
options={[ ...environments.map(env => ({ value: env, label: env }))]}
onChange={handleFilterChange}
className="flex-1 min-w-[200px]"
/>
<FilterSelect
label="状态"
name="is_active"
value={searchParams.get('is_active') || ''}
options={[
{ 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.name}</div>
</div>
<div className="config-detail-item">
<div className="config-detail-label"></div>
<div className="config-detail-value">{selectedConfig.type}</div>
</div>
<div className="config-detail-item">
<div className="config-detail-label"></div>
<div className="config-detail-value">
<span className="env-tag">
{selectedConfig.environment}
</span>
</div>
</div>
<div className="config-detail-item">
<div className="config-detail-label"></div>
<div className="config-detail-value">
<Tag color={selectedConfig.is_active ? 'green' : 'red'}>
{selectedConfig.is_active ? '已启用' : '已禁用'}
</Tag>
</div>
</div>
<div className="config-detail-item">
<div className="config-detail-label"></div>
<pre className="config-detail-code">
{JSON.stringify(selectedConfig.config, 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.created_at}</div>
</div>
<div className="config-detail-item">
<div className="config-detail-label"></div>
<div className="config-detail-value">{selectedConfig.updated_at}</div>
</div>
</div>
</div>
<div className="flex justify-end mt-6">
<Button type="default" onClick={closeDetailModal}></Button>
</div>
</div>
</div>
)}
</div>
);
}