508 lines
16 KiB
TypeScript
508 lines
16 KiB
TypeScript
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>
|
||
);
|
||
} |