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

508 lines
16 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 { type MetaFunction, type LoaderFunctionArgs, type ActionFunctionArgs } from "@remix-run/node";
import { useLoaderData, useSearchParams, useFetcher, Link } from "@remix-run/react";
import { useState, useEffect } from "react";
import { Button } from "~/components/ui/Button";
import { Card } from "~/components/ui/Card";
import { FilterPanel, FilterSelect, SearchFilter } from "~/components/ui/FilterPanel";
import { Modal } from "~/components/ui/Modal";
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";
import { toastService } from "~/components/ui/Toast";
import { messageService } from "~/components/ui/MessageModal";
import { getUserSession } from "~/api/login/auth.server";
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 ActionResponse {
result: boolean;
message: 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);
// 获取JWT token
const { frontendJWT } = await getUserSession(request);
try {
// 获取配置列表
const configsResponse = await getConfigLists({
name,
type,
environment,
is_active,
page: currentPage,
pageSize
}, frontendJWT);
if (configsResponse.error || !configsResponse.data) {
throw new Error(configsResponse.error || "获取配置列表失败");
}
// 获取配置选项
const optionsResponse = await getConfigOptions(frontendJWT);
if (optionsResponse.error || !optionsResponse.data) {
throw new Error(optionsResponse.error || "获取配置选项失败");
}
return Response.json({
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);
return Response.json({
error: error || '加载配置列表失败',
status: 500
}, { 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 Response.json({ result: false, message: "缺少配置ID" }, { status: 400 });
}
// 获取JWT token
const { frontendJWT } = await getUserSession(request);
// 进行更新启用和禁用的状态
try {
if (_action === 'toggleStatus') {
const is_active = formData.get('is_active') === 'true';
const response = await updateConfigStatus(parseInt(configId as string), is_active, frontendJWT);
if (response.error) {
return Response.json({ result: false, message: response.error }, { status: 500 });
}
return Response.json({ result: true, message: is_active ? '启用成功' : '禁用成功' });
}
return Response.json({ result: false, message: "未知操作" }, { status: 400 });
} catch (error) {
console.error('操作配置失败:', error);
return Response.json({ result: false, message: error || "操作失败" }, { status: 500 });
}
}
export default function ConfigListsIndex() {
const { configs, totalCount, currentPage, pageSize, types, environments, error } = useLoaderData<typeof loader>();
const [searchParams, setSearchParams] = useSearchParams();
const fetcher = useFetcher<ActionResponse>();
const [showDetailModal, setShowDetailModal] = useState(false);
const [selectedConfig, setSelectedConfig] = useState<ConfigItem | null>(null);
// 处理loader错误
useEffect(() => {
if(error) {
toastService.error(error);
}
}, [error]);
// 使用useEffect监听fetcher状态变化并显示Toast
useEffect(() => {
if(fetcher.state === 'idle' && fetcher.data) {
if(fetcher.data.result) {
toastService.success(fetcher.data.message);
} else if (fetcher.data.message) {
toastService.error(fetcher.data.message);
}
}
}, [fetcher.state,fetcher.data]);
// 处理筛选条件变化
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);
value ? newParams.set('name', value) : newParams.delete('name');
// 搜索时,重置到第一页
newParams.set('page', '1');
setSearchParams(newParams);
};
// 处理启用和禁用状态
const handleToggleStatus = (config: ConfigItem) => {
messageService.show({
title: '提示',
message: `确定要${config.is_active ? '禁用' : '启用'}该配置吗?`,
type: config.is_active ? 'warning' : 'success',
confirmText: '确定',
cancelText: '取消',
onConfirm: () => {
const formData = new FormData();
formData.append('_action', 'toggleStatus');
formData.append('configId', config.id.toString());
formData.append('is_active', String(!config.is_active));
fetcher.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;
if(nameInput) nameInput.value = ''
setSearchParams(new URLSearchParams());
};
// 关闭详情模态框
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: string) => ({ value: type, label: type }))]}
onChange={handleFilterChange}
className="flex-1 min-w-[200px]"
/>
<FilterSelect
label="环境"
name="environment"
value={searchParams.get('environment') || ''}
options={[ ...environments.map((env: string) => ({ 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 && (
<Modal
isOpen={showDetailModal}
onClose={closeDetailModal}
title="查看配置详情"
width="800px"
footer={
<Button type="default" onClick={closeDetailModal}></Button>
}
>
<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>
</Modal>
)}
</div>
);
}
// 错误边界
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>
);
}