Merge branch 'shiy' into awen

# Conflicts:
#	app/root.tsx
This commit is contained in:
2025-03-28 15:44:17 +08:00
44 changed files with 3802 additions and 1041 deletions
+4 -3
View File
@@ -3,9 +3,10 @@ import { type MetaFunction } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
import { Card } from "~/components/ui/Card";
import { Button } from "~/components/ui/Button";
import homeStyles from "~/styles/pages/home.css?url";
export const links = () => [
{ rel: "stylesheet", href: "app/styles/pages/home.css" }
{ rel: "stylesheet", href: homeStyles }
];
export const meta: MetaFunction = () => {
@@ -255,7 +256,7 @@ function StatusBadge({ status }: StatusBadgeProps) {
warning: {
label: '警告',
className: 'status-badge status-warning',
icon: 'ri-error-warning-line'
icon: 'ri-alert-line'
},
fail: {
label: '不通过',
@@ -264,7 +265,7 @@ function StatusBadge({ status }: StatusBadgeProps) {
},
pending: {
label: '待确认',
className: 'status-badge',
className: 'status-badge status-processing',
icon: 'ri-time-line'
}
};
+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>
);
}
+682
View File
@@ -0,0 +1,682 @@
import { json, redirect, type ActionFunctionArgs, type LoaderFunctionArgs, type MetaFunction } from "@remix-run/node";
import { Form, useActionData, useLoaderData, useNavigation } from "@remix-run/react";
import { useEffect, useState } from "react";
import { Button } from "~/components/ui/Button";
import { Card } from "~/components/ui/Card";
import { ConfigModule, MODULE_LABELS, ENVIRONMENT_LABELS } from "./config-lists._index";
import configNewStyles from "~/styles/pages/config-lists_new.css?url";
export const links = () => [
{ rel: "stylesheet", href: configNewStyles }
];
export const handle = {
breadcrumb: ({ location }: { location: Location }) => {
const hasId = new URLSearchParams(location.search).has("id");
return hasId ? "编辑配置" : "新增配置";
}
};
export const meta: MetaFunction = () => {
return [
{ title: "新增配置 - 中国烟草AI合同及卷宗审核系统" },
{ name: "description", content: "新增或编辑系统配置项" },
{ name: "keywords", content: "配置管理,系统配置,新增配置,编辑配置" }
];
};
// 扩展环境枚举,添加"通用"选项
export enum ExtendedConfigEnvironment {
DEV = 'dev',
TEST = 'test',
PROD = 'prod',
COMMON = 'common'
}
// 扩展环境标签映射
export const EXTENDED_ENVIRONMENT_LABELS: Record<string, string> = {
...ENVIRONMENT_LABELS,
[ExtendedConfigEnvironment.COMMON]: '通用'
};
interface ConfigData {
id: string;
configName: string;
module: ConfigModule;
environment: string; // 使用扩展的环境类型
isActive: boolean;
configData: string; // JSON字符串
remarks?: string; // 添加备注字段
}
interface LoaderData {
config?: ConfigData;
isEdit: boolean;
}
export async function loader({ request }: LoaderFunctionArgs) {
const url = new URL(request.url);
const id = url.searchParams.get("id");
let config: ConfigData | undefined = undefined;
if (id) {
try {
// 实际应用中,应从API获取配置详情
// const response = await fetch(`${process.env.API_BASE_URL}/api/configs/${id}`);
// if (!response.ok) throw new Error(`获取配置详情失败: ${response.status}`);
// config = await response.json();
// config.configData = JSON.stringify(config.configData, null, 2);
// 使用模拟数据
if (id === "1") {
config = {
id: "1",
configName: "database_connection",
module: ConfigModule.SYSTEM,
environment: ExtendedConfigEnvironment.PROD,
isActive: true,
remarks: "数据库连接配置,包含主库和从库配置",
configData: JSON.stringify({
database: {
host: "db.cluster.com",
port: 5432,
pool_size: 20,
ssl: true
},
cache: {
ttl: 3600,
max_entries: 1000
},
feature_flags: ["new_ui", "analytics_v2"]
}, null, 2)
};
} else if (id === "2") {
config = {
id: "2",
configName: "text_extraction_ai",
module: ConfigModule.AI,
environment: ExtendedConfigEnvironment.TEST,
isActive: true,
remarks: "AI文本抽取服务配置",
configData: JSON.stringify({
model: "gpt-4",
parameters: {
temperature: 0.7,
max_tokens: 2000
},
api_key: "sk-**********",
timeout: 30
}, null, 2)
};
} else if (id === "3") {
config = {
id: "3",
configName: "notification_service",
module: ConfigModule.NOTIFICATION,
environment: ExtendedConfigEnvironment.DEV,
isActive: false,
remarks: "通知服务配置,目前处于开发测试阶段",
configData: JSON.stringify({
email: {
smtp_server: "smtp.example.com",
port: 587,
use_tls: true,
sender: "noreply@example.com"
},
sms: {
provider: "aliyun",
region: "cn-hangzhou",
sign_name: "AI审核系统"
}
}, null, 2)
};
} else if (id === "4") {
config = {
id: "4",
configName: "file_storage",
module: ConfigModule.FILE,
environment: ExtendedConfigEnvironment.COMMON,
isActive: true,
remarks: "文件存储通用配置,适用于所有环境",
configData: JSON.stringify({
type: "oss",
region: "cn-shanghai",
bucket: "contracts-ai-review",
access_control: "private",
lifecycle_rules: [
{
prefix: "temp/",
ttl_days: 7
}
]
}, null, 2)
};
}
} catch (error) {
console.error("获取配置详情失败:", error);
// 在实际应用中,应该将错误信息返回给客户端
// 这里简单处理,返回空config
}
}
return json<LoaderData>({
config,
isEdit: !!config
});
}
interface ActionData {
success?: boolean;
errors?: {
configName?: string;
module?: string;
environment?: string;
configData?: string;
general?: string;
};
}
export async function action({ request }: ActionFunctionArgs) {
const formData = await request.formData();
const configId = formData.get("id") as string;
const configName = formData.get("configName") as string;
const module = formData.get("module") as string;
const environment = formData.get("environment") as string;
const configData = formData.get("configData") as string;
const isActive = formData.get("isActive") === "true";
const remarks = formData.get("remarks") as string;
const errors: ActionData["errors"] = {};
// 表单验证
if (!configName || configName.trim() === "") {
errors.configName = "配置名称不能为空";
}
if (!module) {
errors.module = "请选择所属模块";
}
if (!environment) {
errors.environment = "请选择环境";
}
if (!configData || configData.trim() === "") {
errors.configData = "配置数据不能为空";
} else {
try {
JSON.parse(configData);
} catch (e) {
errors.configData = "配置数据必须是有效的JSON格式";
}
}
if (Object.keys(errors).length > 0) {
return json<ActionData>({ errors });
}
try {
// 实际应用中,应调用API保存数据
console.log("保存配置:", { configId, configName, module, environment, configData, isActive, remarks });
// 模拟API调用
// const response = await fetch(`${process.env.API_BASE_URL}/api/configs${configId ? `/${configId}` : ''}`, {
// method: configId ? "PUT" : "POST",
// headers: {
// "Content-Type": "application/json",
// },
// body: JSON.stringify({
// id: configId,
// configName,
// module,
// environment,
// configData: JSON.parse(configData),
// isActive,
// remarks,
// }),
// });
//
// if (!response.ok) {
// throw new Error(`保存失败: ${response.status}`);
// }
// 保存成功后重定向到列表页
return redirect("/config-lists");
} catch (error) {
console.error("保存配置失败:", error);
return json<ActionData>({
success: false,
errors: {
general: "保存配置失败,请稍后重试"
}
});
}
}
// JSON模板数据
const JSON_TEMPLATES = {
database: {
database: {
host: "localhost",
port: 5432,
username: "db_user",
password: "******",
name: "app_database",
pool_size: 10,
timeout: 5000,
ssl: false
}
},
file: {
storage: {
type: "local", // or "s3", "oss"
path: "/data/uploads",
allowed_types: ["pdf", "docx", "jpg", "png"],
max_size: 10485760, // 10MB
backup_enabled: true
}
},
ai: {
ai_service: {
provider: "openai",
api_key: "sk-******",
model: "gpt-4",
max_tokens: 2000,
temperature: 0.7,
timeout: 30000,
rate_limit: 10 // requests per minute
}
}
};
export default function ConfigNew() {
const { config, isEdit } = useLoaderData<typeof loader>();
const actionData = useActionData<typeof action>();
const navigation = useNavigation();
const isSubmitting = navigation.state === "submitting";
const [jsonError, setJsonError] = useState<string | null>(null);
const [configDataValue, setConfigDataValue] = useState("");
const [exampleJsonValue, setExampleJsonValue] = useState("");
// 标签选择状态
const [selectedModule, setSelectedModule] = useState<string>("");
const [selectedEnvironment, setSelectedEnvironment] = useState<string>("");
useEffect(() => {
// 初始化配置数据
if (config?.configData) {
setConfigDataValue(config.configData);
}
// 初始化模块和环境的选中状态
if (config?.module) {
setSelectedModule(config.module);
}
if (config?.environment) {
setSelectedEnvironment(config.environment);
}
// 初始化示例JSON
setExampleJsonValue(JSON.stringify({
database: {
host: "db.cluster.com",
port: 5432,
pool_size: 20,
ssl: true
},
cache: {
ttl: 3600,
max_entries: 1000
},
feature_flags: ["new_ui", "analytics_v2"]
}, null, 2));
}, [config]);
// 处理JSON数据变更
const handleConfigDataChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
const value = e.target.value;
setConfigDataValue(value);
if (value.trim() === "") {
setJsonError(null);
return;
}
try {
JSON.parse(value);
setJsonError(null);
} catch (error) {
if (error instanceof Error) {
setJsonError(`配置数据必须是有效的JSON格式: ${error.message}`);
} else {
setJsonError("配置数据必须是有效的JSON格式");
}
}
};
// 格式化JSON
const handleFormatJson = () => {
if (configDataValue.trim() === "") return;
try {
const parsed = JSON.parse(configDataValue);
setConfigDataValue(JSON.stringify(parsed, null, 2));
setJsonError(null);
} catch (error) {
if (error instanceof Error) {
setJsonError(`当前不是有效的JSON,无法格式化: ${error.message}`);
} else {
setJsonError("当前不是有效的JSON,无法格式化");
}
}
};
// 加载JSON模板
const handleLoadTemplate = (type: keyof typeof JSON_TEMPLATES) => {
const template = JSON_TEMPLATES[type];
setConfigDataValue(JSON.stringify(template, null, 2));
setJsonError(null);
};
// 模块标签点击
const handleModuleTagClick = (module: string) => {
setSelectedModule(module);
};
// 环境标签点击
const handleEnvironmentTagClick = (env: string) => {
setSelectedEnvironment(env);
};
// 显示JSON语法高亮
const renderJsonWithSyntaxHighlight = (json: string) => {
try {
// 如果是空字符串,直接返回
if (!json.trim()) return "";
// 解析并格式化JSON
const parsed = JSON.parse(json);
const formatted = JSON.stringify(parsed, null, 2);
// 添加语法高亮
return formatted
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+-]?\d+)?)/g, (match) => {
let cls = 'number';
if (/^"/.test(match)) {
if (/:$/.test(match)) {
cls = 'key';
match = match.replace(':', '');
} else {
cls = 'string';
}
} else if (/true|false/.test(match)) {
cls = 'boolean';
} else if (/null/.test(match)) {
cls = 'null';
}
return `<span class="code-json ${cls}">${match}</span>`;
});
} catch (e) {
// 如果解析失败,返回原始JSON
return json;
}
};
return (
<div className="config-new-page">
<div className="flex justify-between items-center mb-4">
<span className="block text-xl font-medium">{isEdit ? "编辑系统配置" : "新增系统配置"}</span>
<div className="form-actions">
<Button type="default" to="/config-lists">
<i className="ri-arrow-left-line mr-1"></i>
</Button>
<Form method="post" className="inline">
{config?.id && <input type="hidden" name="id" value={config.id} />}
<input type="hidden" name="configName" value={config?.configName || ''} />
<input type="hidden" name="module" value={selectedModule} />
<input type="hidden" name="environment" value={selectedEnvironment} />
<input type="hidden" name="configData" value={configDataValue} />
<input type="hidden" name="isActive" value={config?.isActive !== false ? "true" : "false"} />
<input type="hidden" name="remarks" value={config?.remarks || ''} />
<Button type="primary" disabled={isSubmitting}>
<i className="ri-save-line mr-1"></i>
{isSubmitting ? '保存中...' : '保存'}
</Button>
</Form>
</div>
</div>
<Card className="config-form-card">
<Form method="post" id="configForm" className="config-form">
{config?.id && <input type="hidden" name="id" value={config.id} />}
{/* 配置名称和状态 */}
<div className="form-row">
<div className="form-group">
<label htmlFor="configName" className="form-label required"></label>
<input
type="text"
id="configName"
name="configName"
className={`form-input ${actionData?.errors?.configName ? 'input-error' : ''}`}
defaultValue={config?.configName || ''}
placeholder="请输入配置名称,如database_connection"
required
/>
{actionData?.errors?.configName && (
<div className="error-message">{actionData.errors.configName}</div>
)}
<div className="form-help">
使使线
</div>
</div>
<div className="form-group">
<label htmlFor="isActive" className="form-label"></label>
<div className="mt-2">
<div className="flex items-center">
<input
type="checkbox"
id="isActive"
name="isActive"
value="true"
className="form-checkbox"
defaultChecked={config?.isActive !== false}
/>
<label htmlFor="isActive" className="form-checkbox-label">
</label>
</div>
<div className="form-help">
</div>
</div>
</div>
</div>
{/* 所属模块 */}
<div className="form-group">
<label htmlFor="module" className="form-label required"></label>
<input
type="hidden"
name="module"
value={selectedModule}
/>
<input
type="text"
id="moduleDisplay"
className={`form-input ${actionData?.errors?.module ? 'input-error' : ''}`}
value={selectedModule ? MODULE_LABELS[selectedModule as ConfigModule] || selectedModule : ''}
placeholder="请输入或选择所属模块"
readOnly
required
/>
{actionData?.errors?.module && (
<div className="error-message">{actionData.errors.module}</div>
)}
<div className="tag-buttons mt-2">
{Object.entries(MODULE_LABELS).map(([value, label]) => (
<button
key={value}
type="button"
className={`tag-button ${selectedModule === value ? 'active' : ''}`}
onClick={() => handleModuleTagClick(value)}
>
{label}
</button>
))}
</div>
<div className="form-help">
便
</div>
</div>
{/* 环境 */}
<div className="form-group">
<label htmlFor="environment" className="form-label required"></label>
<input
type="hidden"
name="environment"
value={selectedEnvironment}
/>
<input
type="text"
id="environmentDisplay"
className={`form-input ${actionData?.errors?.environment ? 'input-error' : ''}`}
value={selectedEnvironment ? EXTENDED_ENVIRONMENT_LABELS[selectedEnvironment] || selectedEnvironment : ''}
placeholder="请输入或选择环境"
readOnly
required
/>
{actionData?.errors?.environment && (
<div className="error-message">{actionData.errors.environment}</div>
)}
<div className="tag-buttons mt-2">
{Object.entries(EXTENDED_ENVIRONMENT_LABELS).map(([value, label]) => (
<button
key={value}
type="button"
className={`tag-button ${selectedEnvironment === value ? 'active' : ''}`}
onClick={() => handleEnvironmentTagClick(value)}
>
{label}
</button>
))}
</div>
<div className="form-help">
使使
</div>
</div>
{/* 配置数据 */}
<div className="form-group">
<label htmlFor="configData" className="form-label required"> (JSON)</label>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4" style={{ minHeight: '390px' }}>
{/* 左侧JSON编辑区 */}
<div className="h-full">
<textarea
id="configData"
name="configData"
className={`json-editor ${(actionData?.errors?.configData || jsonError) ? 'input-error' : ''}`}
value={configDataValue}
onChange={handleConfigDataChange}
required
placeholder='请输入JSON格式的配置数据'
/>
<div className="editor-actions">
<Button
type="default"
size="small"
onClick={handleFormatJson}
>
<i className="ri-braces-line mr-1"></i> JSON
</Button>
</div>
{(actionData?.errors?.configData || jsonError) && (
<div className="error-message">{actionData?.errors?.configData || jsonError}</div>
)}
</div>
{/* 右侧示例区 */}
<div className="h-full">
<div className="example-card">
<div className="example-header">
<div className="example-title"></div>
</div>
<div className="example-content">
<pre
className="example-pre"
dangerouslySetInnerHTML={{ __html: renderJsonWithSyntaxHighlight(exampleJsonValue) }}
/>
</div>
<div className="example-footer">
<div className="text-sm font-medium mb-2">:</div>
<div className="flex flex-wrap gap-2">
<Button
type="default"
size="small"
onClick={() => handleLoadTemplate('database')}
>
</Button>
<Button
type="default"
size="small"
onClick={() => handleLoadTemplate('file')}
>
</Button>
<Button
type="default"
size="small"
onClick={() => handleLoadTemplate('ai')}
>
AI服务配置
</Button>
</div>
</div>
</div>
</div>
</div>
<div className="form-help">
JSON格式的配置数据
</div>
</div>
{/* 备注 */}
<div className="form-group">
<label htmlFor="remarks" className="form-label"></label>
<textarea
id="remarks"
name="remarks"
className="form-textarea"
defaultValue={config?.remarks || ''}
rows={2}
placeholder="请输入配置备注信息"
/>
<div className="form-help">
</div>
</div>
{actionData?.errors?.general && (
<div className="form-row">
<div className="error-message general-error">{actionData.errors.general}</div>
</div>
)}
</Form>
</Card>
</div>
);
}
+23
View File
@@ -0,0 +1,23 @@
import { Outlet } from "@remix-run/react";
import { type MetaFunction } from "@remix-run/node";
export const meta: MetaFunction = () => {
return [
{ title: "配置列表 - 中国烟草AI合同及卷宗审核系统" },
{
name: "config-lists",
content: "配置列表模块,包括配置列表、创建和编辑功能"
}
];
};
export const handle = {
breadcrumb: "配置列表"
};
/**
* 配置列表路由布局
*/
export default function ConfigListsLayout() {
return <Outlet />;
}
+189 -142
View File
@@ -1,11 +1,13 @@
import { json, type MetaFunction } from "@remix-run/node";
import { useLoaderData, Link, useNavigate } from "@remix-run/react";
import { useLoaderData, Link, useNavigate, useSearchParams } from "@remix-run/react";
import { useState } from "react";
import indexStyles from "~/styles/pages/rule-groups_index.css?url";
import { Card } from "~/components/ui/Card";
import { Button } from "~/components/ui/Button";
import { SearchBox } from "~/components/ui/SearchBox";
import { StatusDot } from "~/components/ui/StatusDot";
import { Table } from "~/components/ui/Table";
import { FilterPanel, FilterSelect, SearchFilter } from "~/components/ui/FilterPanel";
import { Pagination } from "~/components/ui/Pagination";
// 定义数据类型
interface RuleGroup {
@@ -103,8 +105,7 @@ export async function loader() {
export default function RuleGroupsIndex() {
const { groups } = useLoaderData<typeof loader>();
const navigate = useNavigate();
const [searchText, setSearchText] = useState("");
const [groupCode, setGroupCode] = useState("");
const [searchParams, setSearchParams] = useSearchParams();
const [expandedGroups, setExpandedGroups] = useState<string[]>([]);
// 处理展开/收起
@@ -131,14 +132,44 @@ export default function RuleGroupsIndex() {
// 处理搜索名称
const handleNameSearch = (value: string) => {
setSearchText(value);
// 实际项目中这里可能需要调用API或过滤本地数据
const newParams = new URLSearchParams(searchParams);
if (value) {
newParams.set('name', value);
} else {
newParams.delete('name');
}
newParams.set('page', '1');
setSearchParams(newParams);
};
// 处理搜索编码
const handleCodeSearch = (value: string) => {
setGroupCode(value);
// 实际项目中这里可能需要调用API或过滤本地数据
const newParams = new URLSearchParams(searchParams);
if (value) {
newParams.set('code', value);
} else {
newParams.delete('code');
}
newParams.set('page', '1');
setSearchParams(newParams);
};
// 处理状态筛选变更
const handleStatusChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const { value } = e.target;
const newParams = new URLSearchParams(searchParams);
if (value) {
newParams.set('status', value);
} else {
newParams.delete('status');
}
newParams.set('page', '1');
setSearchParams(newParams);
};
// 处理重置筛选
const handleReset = () => {
setSearchParams(new URLSearchParams());
};
// 处理表格数据,包括父子级关系
@@ -158,6 +189,98 @@ export default function RuleGroupsIndex() {
return result;
});
// 定义表格列配置
const columns = [
{
title: "分组名称",
key: "name",
width: "400px",
render: (_: unknown, record: (RuleGroup & { isParent?: boolean, parentId?: string })) => (
<div className={`flex items-center ${!record.isParent ? 'ml-8' : ''}`}>
{record.isParent && (
<span
className="expand-icon"
onClick={() => toggleGroup(record.id)}
role="button"
tabIndex={0}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
toggleGroup(record.id);
}
}}
>
<i className={`ri-arrow-${expandedGroups.includes(record.id) ? 'down' : 'right'}-s-line`}></i>
</span>
)}
<Link
to={`/rule-groups/${record.id}/rules`}
className="group-name-link flex items-center ml-1 text-green-800"
>
<i className={`${record.isParent ? 'ri-folder-line' : 'ri-file-list-line'} mr-1 text-green-800`}></i> {record.name}
</Link>
<span className={`group-badge ${record.isParent ? 'parent-badge' : 'child-badge'}`}>
{record.isParent ? '一级分组' : '二级分组'}
</span>
</div>
)
},
{
title: "分组编码",
key: "code",
render: (_: unknown, record: RuleGroup) => record.code
},
{
title: "评查点数量",
key: "ruleCount",
render: (_: unknown, record: RuleGroup) => (
<>
<Link to={`/rule-groups/${record.id}/rules`} className="badge bg-primary text-white">
{record.ruleCount}
</Link>
{record.subGroupCount > 0 && (
<span className="text-secondary text-sm ml-1">
| : {record.subGroupCount}
</span>
)}
</>
)
},
{
title: "状态",
key: "status",
render: (_: unknown, record: RuleGroup) => (
<StatusDot status={record.status === 'active' ? 'success' : 'error'} text={record.status === 'active' ? '启用' : '禁用'} />
)
},
{
title: "创建时间",
key: "createdAt",
render: (_: unknown, record: RuleGroup) => record.createdAt
},
{
title: "操作",
key: "operation",
width: "180px",
render: (_: unknown, record: RuleGroup) => (
<>
<button
className="ant-btn ant-btn-text ant-btn-sm text-primary"
onClick={() => navigate(`/rule-groups/${record.id}`)}
>
<i className="ri-edit-line"></i>
</button>
<button
className="ant-btn ant-btn-text ant-btn-sm text-error"
onClick={() => handleDeleteGroup(record.id)}
>
<i className="ri-delete-bin-line"></i>
</button>
</>
)
}
];
return (
<div className="content-container rule-groups-page">
{/* 页面头部 */}
@@ -190,146 +313,70 @@ export default function RuleGroupsIndex() {
</div>
</div>
{/* 搜索栏 */}
<Card className="mb-4" bodyClassName="px-4 py-4">
<div className="flex flex-wrap items-end gap-4">
<div className="flex-1 min-w-[200px]">
<label htmlFor="groupName" className="form-label"></label>
<SearchBox
placeholder="请输入分组名称"
defaultValue={searchText}
onSearch={handleNameSearch}
name="groupName"
buttonText=""
className="form-input-only"
/>
</div>
<div className="flex-1 min-w-[200px]">
<label htmlFor="groupCode" className="form-label"></label>
<SearchBox
placeholder="请输入分组编码"
defaultValue={groupCode}
onSearch={handleCodeSearch}
name="groupCode"
buttonText=""
className="form-input-only"
/>
</div>
<div className="flex-1 min-w-[200px]">
<label htmlFor="status" className="form-label"></label>
<select id="status" className="form-select">
<option value=""></option>
<option value="true"></option>
<option value="false"></option>
</select>
</div>
<div className="flex items-center">
<Button type="default" icon="ri-refresh-line" className="mr-2">
{/* 搜索栏 - 使用FilterPanel */}
<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>
</div>
</div>
</Card>
{/* 数据表格 */}
<Card bodyClassName="px-4 py-4">
<div className="overflow-x-auto">
<table className="ant-table tree-table w-full">
<thead>
<tr>
<th style={{ width: "400px" }}></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th style={{ width: "180px" }}></th>
</tr>
</thead>
<tbody>
{processedData.map((item) => (
<tr key={item.id} className={`group-row ${item.isParent ? 'parent-row' : 'child-row child-of-' + item.parentId}`}>
<td>
<div className={`flex items-center ${!item.isParent ? 'ml-8' : ''}`}>
{item.isParent && (
<span
className="expand-icon"
onClick={() => toggleGroup(item.id)}
role="button"
tabIndex={0}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
toggleGroup(item.id);
}
}}
>
<i className={`ri-arrow-${expandedGroups.includes(item.id) ? 'down' : 'right'}-s-line`}></i>
</span>
)}
<Link
to={`/rule-groups/${item.id}/rules`}
className="group-name-link flex items-center ml-1"
>
<i className={`${item.isParent ? 'ri-folder-line' : 'ri-file-list-line'} mr-1`}></i> {item.name}
</Link>
<span className={`group-badge ${item.isParent ? 'parent-badge' : 'child-badge'}`}>
{item.isParent ? '一级分组' : '二级分组'}
</span>
</div>
</td>
<td>{item.code}</td>
<td>
<Link to={`/rule-groups/${item.id}/rules`} className="badge bg-primary text-white">
{item.ruleCount}
</Link>
{item.subGroupCount > 0 && (
<span className="text-secondary text-sm ml-1">
| : {item.subGroupCount}
</span>
)}
</td>
<td>
<StatusDot status={item.status === 'active' ? 'success' : 'error'} text={item.status === 'active' ? '启用' : '禁用'} />
</td>
<td>{item.createdAt}</td>
<td>
<button
className="ant-btn ant-btn-text ant-btn-sm text-primary"
onClick={() => navigate(`/rule-groups/${item.id}`)}
>
<i className="ri-edit-line"></i>
</button>
<button
className="ant-btn ant-btn-text ant-btn-sm text-error"
onClick={() => handleDeleteGroup(item.id)}
>
<i className="ri-delete-bin-line"></i>
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</>
}
noActionDivider={true}
>
<SearchFilter
label="分组名称"
placeholder="请输入分组名称"
value={searchParams.get('name') || ''}
onSearch={handleNameSearch}
className="flex-1 min-w-[200px]"
instantSearch={true}
/>
{/* 分页 */}
<div className="flex justify-between items-center mt-4">
<div className="text-sm text-secondary">
{groups.length} 10
</div>
<div className="ant-pagination">
<button className="ant-pagination-item ant-pagination-prev" disabled>
<i className="ri-arrow-left-s-line"></i>
</button>
<button className="ant-pagination-item ant-pagination-item-active">1</button>
<button className="ant-pagination-item ant-pagination-next" disabled>
<i className="ri-arrow-right-s-line"></i>
</button>
</div>
</div>
<SearchFilter
label="分组编码"
placeholder="请输入分组编码"
value={searchParams.get('code') || ''}
onSearch={handleCodeSearch}
className="flex-1 min-w-[200px]"
instantSearch={true}
/>
<FilterSelect
label="状态"
name="status"
value={searchParams.get('status') || ''}
options={[
{ value: "active", label: "启用" },
{ value: "inactive", label: "禁用" }
]}
onChange={handleStatusChange}
className="flex-1 min-w-[200px]"
/>
</FilterPanel>
{/* 数据表格 - 使用Table组件 */}
<Card bodyClassName="px-4 py-4">
<Table
columns={columns}
dataSource={processedData}
rowKey="id"
emptyText="暂无分组数据"
className="tree-table"
/>
{/* 分页 - 使用Pagination组件 */}
<Pagination
currentPage={1}
total={groups.length}
pageSize={10}
onChange={() => {}}
showTotal={true}
/>
</Card>
</div>
);
@@ -1,15 +1,18 @@
import { json, type MetaFunction, type LoaderFunctionArgs } from "@remix-run/node";
import { useLoaderData, useSearchParams, useSubmit } from "@remix-run/react";
import { useLoaderData, useSearchParams } from "@remix-run/react";
import { Button } from "~/components/ui/Button";
import { Card } from "~/components/ui/Card";
import { FileIcon } from "~/components/ui/FileIcon";
import { FilterPanel, FilterSelect, SearchFilter } from "~/components/ui/FilterPanel";
import { Pagination } from "~/components/ui/Pagination";
import { Table } from "~/components/ui/Table";
import { SearchBox } from "~/components/ui/SearchBox";
import rulesFilesStyles from "~/styles/pages/rules_files.css?url";
import rulesFilesStyles from "~/styles/pages/rules-files.css?url";
export const links = () => [
{ rel: "stylesheet", href: rulesFilesStyles }
];
export const handle = {
breadcrumb: "评查文件列表"
};
@@ -196,7 +199,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
return fileDate >= today;
});
break;
case DateRange.WEEK:
case DateRange.WEEK: {
const weekStart = new Date(today);
weekStart.setDate(today.getDate() - today.getDay());
filteredFiles = filteredFiles.filter(file => {
@@ -204,13 +207,15 @@ export async function loader({ request }: LoaderFunctionArgs) {
return fileDate >= weekStart;
});
break;
case DateRange.MONTH:
}
case DateRange.MONTH: {
const monthStart = new Date(today.getFullYear(), today.getMonth(), 1);
filteredFiles = filteredFiles.filter(file => {
const fileDate = new Date(file.uploadTime.split(' ')[0]);
return fileDate >= monthStart;
});
break;
}
}
}
@@ -264,18 +269,20 @@ export async function loader({ request }: LoaderFunctionArgs) {
}
}
// 提取renderErrorBoundary函数作为命名导出
export function ErrorBoundary() {
return (
<div className="error-container p-6">
<h1 className="text-xl font-bold text-red-500 mb-4"></h1>
<h1 className="text-xl font-normal text-red-500 mb-4"></h1>
<p className="mb-4"></p>
<Button type="primary" to="/"></Button>
</div>
);
}
export default function ReviewFilesList() {
const { files, totalCount, currentPage, pageSize, totalPages } = useLoaderData<typeof loader>();
// 在文件中定义一个与路由文件名匹配的命名函数组件
export default function RulesFiles() {
const { files, totalCount, currentPage, pageSize } = useLoaderData<typeof loader>();
const [searchParams, setSearchParams] = useSearchParams();
const handleFilterChange = (e: React.ChangeEvent<HTMLSelectElement | HTMLInputElement>) => {
@@ -314,6 +321,14 @@ export default function ReviewFilesList() {
setSearchParams(newParams);
};
// 添加页码大小变更处理函数
const handlePageSizeChange = (size: number) => {
const newParams = new URLSearchParams(searchParams);
newParams.set('pageSize', size.toString());
newParams.set('page', '1'); // 改变每页条数时重置为第一页
setSearchParams(newParams);
};
// 渲染问题摘要
const renderIssues = (issues: ReviewFile['issues']) => {
if (issues.length === 0) {
@@ -327,7 +342,7 @@ export default function ReviewFilesList() {
return (
<div className="text-sm">
{issues.slice(0, 3).map((issue, index) => (
<div key={index} className="mb-1 last:mb-0">
<div key={index} className={`mb-1 ${index === issues.length - 1 ? 'last:mb-0' : ''}`}>
<span className={`severity-indicator severity-${issue.severity}`}></span>
{issue.message}
</div>
@@ -336,29 +351,145 @@ export default function ReviewFilesList() {
);
};
// 渲染文件图标
const renderFileIcon = (fileName: string) => {
if (fileName.endsWith('.pdf')) {
return <i className="ri-file-pdf-line text-red-500 mr-2 text-lg"></i>;
} else if (fileName.endsWith('.docx') || fileName.endsWith('.doc')) {
return <i className="ri-file-word-2-line text-blue-500 mr-2 text-lg"></i>;
} else if (fileName.endsWith('.xlsx') || fileName.endsWith('.xls')) {
return <i className="ri-file-excel-2-line text-green-500 mr-2 text-lg"></i>;
} else {
return <i className="ri-file-line text-gray-500 mr-2 text-lg"></i>;
// 文件类型选项
const fileTypeOptions = Object.keys(FILE_TYPE_LABELS).map(type => ({
value: type,
label: FILE_TYPE_LABELS[type as FileType]
}));
// 评查状态选项
const reviewStatusOptions = Object.keys(REVIEW_STATUS_LABELS).map(status => ({
value: status,
label: REVIEW_STATUS_LABELS[status as ReviewStatus]
}));
// 时间范围选项
const dateRangeOptions = [
{ value: DateRange.TODAY, label: '今天' },
{ value: DateRange.WEEK, label: '本周' },
{ value: DateRange.MONTH, label: '本月' },
{ value: DateRange.CUSTOM, label: '自定义时间段' }
];
// 获取文件状态对应的图标和类名
const getStatusInfo = (status: ReviewStatus) => {
switch (status) {
case ReviewStatus.PASS:
return { icon: "ri-checkbox-circle-line", className: "success" };
case ReviewStatus.WARNING:
return { icon: "ri-alert-line", className: "warning" };
case ReviewStatus.FAIL:
return { icon: "ri-close-circle-line", className: "error" };
case ReviewStatus.PENDING:
return { icon: "ri-time-line", className: "processing" };
default:
return { icon: "", className: "default" };
}
};
// 定义表格列配置
const columns = [
{
title: "文件名称",
key: "fileName",
width: "30%",
render: (_: unknown, file: ReviewFile) => (
<div className="flex items-center">
<FileIcon fileName={file.fileName} className="mr-2 text-lg" />
<div>
<div className="font-normal text-base">{file.fileName}</div>
<div className="text-xs text-secondary mt-1">
{file.fileType === FileType.CONTRACT && "合同编号:"}
{file.fileType === FileType.LICENSE && "许可证号:"}
{file.fileType === FileType.PUNISHMENT && "文号:"}
{file.fileType === FileType.REPORT && "报表编号:"}
{file.fileCode}
</div>
</div>
</div>
)
},
{
title: "文件类型",
key: "fileType",
width: "12%",
render: (_: unknown, file: ReviewFile) => (
<span className={`file-type-tag file-type-tag-${file.fileType}`}>
{file.fileType === FileType.CONTRACT && <i className="ri-file-list-3-line"></i>}
{file.fileType === FileType.LICENSE && <i className="ri-vip-crown-line"></i>}
{file.fileType === FileType.PUNISHMENT && <i className="ri-scales-line"></i>}
{file.fileType === FileType.REPORT && <i className="ri-file-chart-line"></i>}
{file.fileType === FileType.OTHER && <i className="ri-file-line"></i>}
{FILE_TYPE_LABELS[file.fileType]}
</span>
)
},
{
title: "上传时间",
key: "uploadTime",
width: "12%",
render: (_: unknown, file: ReviewFile) => (
<div>
<span className="text-base">{file.uploadTime.split(' ')[0]}</span>
<br />
<span className="text-xs text-secondary">{file.uploadTime.split(' ')[1]}</span>
</div>
)
},
{
title: "评查状态",
key: "reviewStatus",
width: "12%",
render: (_: unknown, file: ReviewFile) => {
const statusInfo = getStatusInfo(file.reviewStatus);
return (
<span className={`status-badge status-badge-${statusInfo.className.replace('status-', '')}`}>
<i className={`${statusInfo.icon} mr-1`}></i>
{REVIEW_STATUS_LABELS[file.reviewStatus]}
{file.issueCount > 0 && ` (${file.issueCount})`}
</span>
);
}
},
{
title: "问题摘要",
key: "issues",
width: "20%",
render: (_: unknown, file: ReviewFile) => renderIssues(file.issues)
},
{
title: "操作",
key: "operation",
width: "14%",
render: (_: unknown, file: ReviewFile) => (
<>
{file.reviewStatus === ReviewStatus.PENDING ? (
<Button type="primary" size="small" icon="ri-check-double-line" className="mr-2">
</Button>
) : (
<Button type="default" size="small" icon="ri-eye-line" to={`/files/${file.id}`} className="mr-2">
</Button>
)}
<Button type="default" size="small" icon="ri-download-2-line">
</Button>
</>
)
}
];
return (
<div className="p-6 review-files-page">
{/* 页面头部 */}
<div className="flex justify-between items-center mb-4">
<div className="flex items-center">
<h2 className="text-xl font-medium"></h2>
<h2 className="text-xl font-normal"></h2>
<div className="flex items-center ml-4 bg-white px-3 py-1 rounded-md">
<i className="ri-file-list-3-line text-primary text-lg mr-1"></i>
<span className="text-sm text-secondary"></span>
<span className="text-base font-bold text-primary ml-1">{totalCount}</span>
<span className="text-base font-normal text-primary ml-1">{totalCount}</span>
</div>
</div>
<Button type="primary" icon="ri-file-upload-line" to="/files/new">
@@ -367,218 +498,80 @@ export default function ReviewFilesList() {
</div>
{/* 筛选区域 */}
<Card className="card-container">
<div className="flex flex-wrap items-end gap-3">
<div className="w-48">
<div className="mb-1 text-sm font-medium"></div>
<select
className="form-select w-full"
name="fileType"
value={searchParams.get('fileType') || ''}
onChange={handleFilterChange}
>
<option value=""></option>
<option value={FileType.CONTRACT}></option>
<option value={FileType.LICENSE}></option>
<option value={FileType.PUNISHMENT}></option>
<option value={FileType.REPORT}></option>
<option value={FileType.OTHER}></option>
</select>
</div>
<div className="w-48">
<div className="mb-1 text-sm font-medium"></div>
<select
className="form-select w-full"
name="reviewStatus"
value={searchParams.get('reviewStatus') || ''}
onChange={handleFilterChange}
>
<option value=""></option>
<option value={ReviewStatus.PASS}></option>
<option value={ReviewStatus.WARNING}></option>
<option value={ReviewStatus.FAIL}></option>
<option value={ReviewStatus.PENDING}></option>
</select>
</div>
<div className="w-48">
<div className="mb-1 text-sm font-medium"></div>
<select
className="form-select w-full"
name="dateRange"
value={searchParams.get('dateRange') || ''}
onChange={handleFilterChange}
>
<option value=""></option>
<option value={DateRange.TODAY}></option>
<option value={DateRange.WEEK}></option>
<option value={DateRange.MONTH}></option>
<option value={DateRange.CUSTOM}></option>
</select>
</div>
<div className="w-72">
<div className="mb-1 text-sm font-medium"></div>
<div className="flex border border-gray-300 rounded overflow-hidden">
<SearchBox
placeholder="搜索文件名、合同编号或关键词"
defaultValue={searchParams.get('keyword') || ''}
onSearch={handleSearch}
className="search-input"
buttonText="搜索"
/>
</div>
</div>
<FilterPanel className="px-3 py-3" noActionDivider={true}>
<FilterSelect
label="文件类型"
name="fileType"
value={searchParams.get('fileType') || ''}
options={fileTypeOptions}
onChange={handleFilterChange}
className="mr-2 w-40"
/>
<FilterSelect
label="评查状态"
name="reviewStatus"
value={searchParams.get('reviewStatus') || ''}
options={reviewStatusOptions}
onChange={handleFilterChange}
className="mr-2 w-40"
/>
<FilterSelect
label="时间范围"
name="dateRange"
value={searchParams.get('dateRange') || ''}
options={dateRangeOptions}
onChange={handleFilterChange}
className="mr-2 w-40"
/>
<SearchFilter
label="搜索"
placeholder="搜索文件名、合同编号或关键词"
value={searchParams.get('keyword') || ''}
onSearch={handleSearch}
buttonText=""
className="mr-2 w-64"
/>
<div className="ml-auto">
<select
className="form-select w-auto"
name="sortOrder"
value={searchParams.get('sortOrder') || 'upload_time_desc'}
onChange={handleFilterChange}
>
<option value="upload_time_desc"> </option>
<option value="upload_time_asc"> </option>
<option value="issue_count_desc"> </option>
<option value="issue_count_asc"> </option>
</select>
</div>
</div>
</Card>
<FilterSelect
label="排序方式"
name="sortOrder"
value={searchParams.get('sortOrder') || 'upload_time_desc'}
onChange={handleFilterChange}
className="w-32"
options={[
{ value: "upload_time_desc", label: "上传时间 ↓" },
{ value: "upload_time_asc", label: "上传时间 ↑" },
{ value: "issue_count_desc", label: "问题数量 ↓" },
{ value: "issue_count_asc", label: "问题数量 ↑" }
]}
/>
</FilterPanel>
{/* 文件列表 */}
<Card className="content-card">
<table className="ant-table">
<thead>
<tr>
<th style={{ width: "30%" }}></th>
<th style={{ width: "12%" }}></th>
<th style={{ width: "12%" }}></th>
<th style={{ width: "12%" }}></th>
<th style={{ width: "20%" }}></th>
<th style={{ width: "14%" }}></th>
</tr>
</thead>
<tbody>
{files.length > 0 ? (
files.map((file) => (
<tr key={file.id}>
<td>
<div className="flex items-center">
{renderFileIcon(file.fileName)}
<div>
<div className="font-medium">{file.fileName}</div>
<div className="text-xs text-secondary mt-1">
{file.fileType === FileType.CONTRACT && "合同编号:"}
{file.fileType === FileType.LICENSE && "许可证号:"}
{file.fileType === FileType.PUNISHMENT && "文号:"}
{file.fileType === FileType.REPORT && "报表编号:"}
{file.fileCode}
</div>
</div>
</div>
</td>
<td>
<span className={`file-type-badge file-type-${file.fileType}`}>
{file.fileType === FileType.CONTRACT && <i className="ri-file-list-3-line"></i>}
{file.fileType === FileType.LICENSE && <i className="ri-vip-crown-line"></i>}
{file.fileType === FileType.PUNISHMENT && <i className="ri-scales-line"></i>}
{file.fileType === FileType.REPORT && <i className="ri-file-chart-line"></i>}
{file.fileType === FileType.OTHER && <i className="ri-file-line"></i>}
{FILE_TYPE_LABELS[file.fileType]}
</span>
</td>
<td>
{file.uploadTime.split(' ')[0]}
<br />
<span className="text-xs text-secondary">{file.uploadTime.split(' ')[1]}</span>
</td>
<td>
<span className={`status-badge status-${file.reviewStatus}`}>
{file.reviewStatus === ReviewStatus.PASS && <i className="ri-checkbox-circle-line mr-1"></i>}
{file.reviewStatus === ReviewStatus.WARNING && <i className="ri-alert-line mr-1"></i>}
{file.reviewStatus === ReviewStatus.FAIL && <i className="ri-close-circle-line mr-1"></i>}
{file.reviewStatus === ReviewStatus.PENDING && <i className="ri-time-line mr-1"></i>}
{REVIEW_STATUS_LABELS[file.reviewStatus]}
{file.issueCount > 0 && ` (${file.issueCount})`}
</span>
</td>
<td>
{renderIssues(file.issues)}
</td>
<td>
{file.reviewStatus === ReviewStatus.PENDING ? (
<Button type="primary" size="small" icon="ri-check-double-line" className="mr-2">
</Button>
) : (
<Button type="default" size="small" icon="ri-eye-line" to={`/files/${file.id}`} className="mr-2">
</Button>
)}
<Button type="default" size="small" icon="ri-download-2-line">
</Button>
</td>
</tr>
))
) : (
<tr>
<td colSpan={6} className="text-center py-8 text-gray-500">
</td>
</tr>
)}
</tbody>
</table>
<Card >
<Table
columns={columns}
dataSource={files}
rowKey="id"
emptyText="暂无文件数据"
className="files-table"
/>
{/* 分页 */}
{/* 分页组件 */}
{totalCount > 0 && (
<div className="pagination">
<button
className={`pagination-item ${currentPage <= 1 ? 'disabled' : ''}`}
onClick={() => currentPage > 1 && handlePageChange(currentPage - 1)}
disabled={currentPage <= 1}
>
<i className="ri-arrow-left-s-line"></i>
</button>
{Array.from({ length: Math.min(totalPages, 5) }, (_, i) => {
// 显示当前页附近的页码,最多显示5个
let pageNum;
if (totalPages <= 5) {
// 总页数少于5,直接显示所有页码
pageNum = i + 1;
} else if (currentPage <= 3) {
// 当前页靠近开始
pageNum = i + 1;
} else if (currentPage >= totalPages - 2) {
// 当前页靠近结尾
pageNum = totalPages - 4 + i;
} else {
// 当前页在中间
pageNum = currentPage - 2 + i;
}
return (
<button
key={pageNum}
className={`pagination-item ${pageNum === currentPage ? 'active' : ''}`}
onClick={() => handlePageChange(pageNum)}
>
{pageNum}
</button>
);
})}
<button
className={`pagination-item ${currentPage >= totalPages ? 'disabled' : ''}`}
onClick={() => currentPage < totalPages && handlePageChange(currentPage + 1)}
disabled={currentPage >= totalPages}
>
<i className="ri-arrow-right-s-line"></i>
</button>
</div>
<Pagination
currentPage={currentPage}
total={totalCount}
pageSize={pageSize}
onChange={handlePageChange}
onPageSizeChange={handlePageSizeChange}
showTotal={true}
showPageSizeChanger={true}
pageSizeOptions={[10, 20, 30, 50]}
/>
)}
</Card>
</div>
@@ -14,7 +14,7 @@ export const meta: MetaFunction = () => {
};
export const handle = {
breadcrumb: '规则详情'
breadcrumb: '编辑评查点'
};
interface LoaderData {
+161 -194
View File
@@ -3,7 +3,6 @@ import { json, type MetaFunction, type LoaderFunctionArgs, redirect } from "@rem
import { useLoaderData, useSearchParams, useSubmit } from "@remix-run/react";
import { Button } from '~/components/ui/Button';
import { Card } from '~/components/ui/Card';
import { SearchBox } from '~/components/ui/SearchBox';
import { Tag } from '~/components/ui/Tag';
import { StatusDot } from '~/components/ui/StatusDot';
import rulesStyles from "~/styles/pages/rules_index.css?url";
@@ -11,14 +10,16 @@ import type { Rule } from '~/models/rule';
import { RULE_TYPE_LABELS, RULE_TYPE_COLORS, RULE_PRIORITY_LABELS, RULE_PRIORITY_COLORS } from '~/models/rule';
import type { TagColor } from '~/components/ui/Tag';
import { Link } from '@remix-run/react';
import { Table } from '~/components/ui/Table';
import { FilterPanel, FilterSelect, SearchFilter } from '~/components/ui/FilterPanel';
import { Pagination } from '~/components/ui/Pagination';
export const links = () => [
{ rel: "stylesheet", href: rulesStyles }
];
export const handle = {
breadcrumb: "评查点列表"
};
// export const handle = {
// breadcrumb: "评查点列表"
// };
export const meta: MetaFunction = () => {
return [
@@ -301,8 +302,8 @@ const priorityLabels = {
'low': '低'
};
export default function RulesList() {
const { rules, groups, totalCount, currentPage, pageSize, totalPages } = useLoaderData<typeof loader>();
export default function RulesIndex() {
const { rules, groups, totalCount, currentPage, pageSize } = useLoaderData<typeof loader>();
const [searchParams, setSearchParams] = useSearchParams();
const submit = useSubmit();
@@ -371,213 +372,179 @@ export default function RulesList() {
setSearchParams(newParams);
};
const handlePageSizeChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const newPageSize = e.target.value;
const handlePageSizeChange = (size: number) => {
const newParams = new URLSearchParams(searchParams);
newParams.set('pageSize', newPageSize);
newParams.set('pageSize', size.toString());
newParams.set('page', '1'); // 更改每页条数时,重置到第一页
setSearchParams(newParams);
};
// 处理重置筛选
const handleReset = () => {
setSearchParams(new URLSearchParams());
};
// 定义表格列配置
const columns = [
{
title: "评查点编码",
dataIndex: "code" as keyof Rule,
key: "code",
align: "center" as const
},
{
title: "评查点名称",
dataIndex: "name" as keyof Rule,
key: "name",
align: "center" as const
},
{
title: "评查点类型",
key: "ruleType",
align: "center" as const,
render: (_: unknown, record: Rule) => {
const typeColor = RULE_TYPE_COLORS[record.ruleType] as TagColor;
return (
<Tag color={typeColor}>
{typeLabels[record.ruleType as keyof typeof typeLabels] || RULE_TYPE_LABELS[record.ruleType]}
</Tag>
);
}
},
{
title: "所属规则组",
dataIndex: "groupName" as keyof Rule,
key: "groupName",
align: "center" as const
},
{
title: "优先级",
key: "priority",
align: "center" as const,
render: (_: unknown, record: Rule) => {
const priorityColor = RULE_PRIORITY_COLORS[record.priority] as TagColor;
return (
<Tag color={priorityColor}>
{priorityLabels[record.priority as keyof typeof priorityLabels] || RULE_PRIORITY_LABELS[record.priority]}
</Tag>
);
}
},
{
title: "状态",
key: "isActive",
align: "center" as const,
className: "status-column",
render: (_: unknown, record: Rule) => (
<StatusDot status={record.isActive} text={record.isActive ? "启用" : "禁用"} />
)
},
{
title: "创建时间",
dataIndex: "createdAt" as keyof Rule,
key: "createdAt",
align: "center" as const
},
{
title: "操作",
key: "operation",
align: "center" as const,
render: (_: unknown, record: Rule) => (
<div className="operations-cell">
<Link to={`/rules/${record.id}`} className="operation-btn">
<i className="ri-edit-line"></i>
</Link>
<button className="operation-btn" onClick={() => handleCopy(record)}>
<i className="ri-file-copy-line"></i>
</button>
<button className="operation-btn operation-btn-danger" onClick={() => handleDeleteClick(record)}>
<i className="ri-delete-bin-line"></i>
</button>
</div>
)
}
];
return (
<div className="p-6 rules-page">
{/* 页面头部 */}
<div className="flex justify-between items-center mb-4">
<h2 className="text-xl font-medium"></h2>
<Button type="primary" icon="ri-add-line" to="/rules/new">
<Button type="primary" icon="ri-add-line" to="/rules/new" className="btn-add-rule">
</Button>
</div>
{/* 筛选区域 */}
<Card className="card-container">
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<div>
<label htmlFor="ruleType" className="form-label"></label>
<select
id="ruleType"
className="form-select"
name="ruleType"
value={searchParams.get('ruleType') || ''}
onChange={handleFilterChange}
>
<option value=""></option>
<option value="essential"></option>
<option value="content"></option>
<option value="format"></option>
<option value="legal"></option>
<option value="business"></option>
</select>
</div>
<div>
<label htmlFor="groupId" className="form-label"></label>
<select
id="groupId"
className="form-select"
name="groupId"
value={searchParams.get('groupId') || ''}
onChange={handleFilterChange}
>
<option value=""></option>
{groups.map((group) => (
<option key={group.id} value={group.id}>{group.name}</option>
))}
</select>
</div>
<div>
<label htmlFor="isActive" className="form-label"></label>
<select
id="isActive"
className="form-select"
name="isActive"
value={searchParams.get('isActive') || ''}
onChange={handleFilterChange}
>
<option value=""></option>
<option value="true"></option>
<option value="false"></option>
</select>
</div>
<div>
<label htmlFor="keyword" className="form-label"></label>
<SearchBox
placeholder="输入评查点名称或编码"
defaultValue={searchParams.get('keyword') || ''}
onSearch={handleSearch}
/>
</div>
</div>
</Card>
<FilterPanel>
<FilterSelect
label="评查点类型"
name="ruleType"
value={searchParams.get('ruleType') || ''}
options={[
{ value: "essential", label: "基本要素类" },
{ value: "content", label: "内容合规类" },
{ value: "format", label: "格式规范类" },
{ value: "legal", label: "法律风险类" },
{ value: "business", label: "业务专项类" }
]}
onChange={handleFilterChange}
className="mr-3 w-80 "
/>
<FilterSelect
label="所属规则组"
name="groupId"
value={searchParams.get('groupId') || ''}
options={groups.map(group => ({ value: group.id, label: group.name }))}
onChange={handleFilterChange}
className="mr-3 w-80"
/>
<FilterSelect
label="状态"
name="isActive"
value={searchParams.get('isActive') || ''}
options={[
{ value: "true", label: "启用" },
{ value: "false", label: "禁用" }
]}
onChange={handleFilterChange}
className="mr-3 w-80"
/>
<SearchFilter
label="搜索"
placeholder="输入评查点名称或编码"
value={searchParams.get('keyword') || ''}
onSearch={handleSearch}
className="flex-1"
/>
</FilterPanel>
{/* 评查点列表 */}
{/* 评查点列表 - 使用Table组件 */}
<Card className="ant-card">
<div className="overflow-x-auto">
<table className="ant-table">
<thead>
<tr>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
{rules.length > 0 ? (
rules.map((rule) => {
const typeColor = RULE_TYPE_COLORS[rule.ruleType] as TagColor;
const priorityColor = RULE_PRIORITY_COLORS[rule.priority] as TagColor;
return (
<tr key={rule.id}>
<td>{rule.code}</td>
<td>{rule.name}</td>
<td>
<Tag color={typeColor}>
{typeLabels[rule.ruleType as keyof typeof typeLabels] || RULE_TYPE_LABELS[rule.ruleType]}
</Tag>
</td>
<td>{rule.groupName}</td>
<td>
<Tag color={priorityColor}>
{priorityLabels[rule.priority as keyof typeof priorityLabels] || RULE_PRIORITY_LABELS[rule.priority]}
</Tag>
</td>
<td>
<StatusDot status={rule.isActive} />
</td>
<td>{rule.createdAt}</td>
<td className="operations-cell">
<Link to={`/rules/${rule.id}`} className="operation-btn">
<i className="ri-edit-line"></i>
</Link>
<button className="operation-btn" onClick={() => handleCopy(rule)}>
<i className="ri-file-copy-line"></i>
</button>
<button className="operation-btn operation-btn-danger" onClick={() => handleDeleteClick(rule)}>
<i className="ri-delete-bin-line"></i>
</button>
</td>
</tr>
);
})
) : (
<tr>
<td colSpan={8} className="text-center py-8 text-gray-500">
</td>
</tr>
)}
</tbody>
</table>
</div>
<Table
columns={columns}
dataSource={rules}
rowKey="id"
emptyText="暂无评查点数据"
className="rules-table"
/>
{/* 分页 */}
{totalCount > 0 && (
<div className="ant-pagination">
<div className="ant-pagination-options">
<span className="text-sm mr-2"> {totalCount} </span>
<select
className="form-select ant-pagination-options-size-changer"
style={{ width: "100px" }}
value={pageSize}
onChange={handlePageSizeChange}
>
<option value="10">10 /</option>
<option value="20">20 /</option>
<option value="50">50 /</option>
</select>
</div>
<div className="ant-pagination-right">
<button
className={`ant-pagination-item ant-pagination-prev ${currentPage <= 1 ? 'ant-pagination-disabled' : ''}`}
onClick={() => currentPage > 1 && handlePageChange(currentPage - 1)}
disabled={currentPage <= 1}
>
<i className="ri-arrow-left-s-line"></i>
</button>
{Array.from({ length: Math.min(totalPages, 5) }, (_, i) => {
// 显示当前页附近的页码,最多显示5个
let pageNum;
if (totalPages <= 5) {
// 总页数少于5,直接显示所有页码
pageNum = i + 1;
} else if (currentPage <= 3) {
// 当前页靠近开始
pageNum = i + 1;
} else if (currentPage >= totalPages - 2) {
// 当前页靠近结尾
pageNum = totalPages - 4 + i;
} else {
// 当前页在中间
pageNum = currentPage - 2 + i;
}
return (
<button
key={pageNum}
className={`ant-pagination-item ${pageNum === currentPage ? 'ant-pagination-item-active' : ''}`}
onClick={() => handlePageChange(pageNum)}
>
{pageNum}
</button>
);
})}
<button
className={`ant-pagination-item ant-pagination-next ${currentPage >= totalPages ? 'ant-pagination-disabled' : ''}`}
onClick={() => currentPage < totalPages && handlePageChange(currentPage + 1)}
disabled={currentPage >= totalPages}
>
<i className="ri-arrow-right-s-line"></i>
</button>
</div>
</div>
<Pagination
currentPage={currentPage}
total={totalCount}
pageSize={pageSize}
onChange={handlePageChange}
onPageSizeChange={handlePageSizeChange}
showTotal={true}
showPageSizeChanger={true}
pageSizeOptions={[10, 20, 30, 50]}
/>
)}
</Card>
+2 -5
View File
@@ -1,22 +1,19 @@
import { Outlet } from "@remix-run/react";
import { type MetaFunction } from "@remix-run/node";
// export const links = () => [
// { rel: "stylesheet", href: "app/styles/pages/rules.css" }
// ];
export const meta: MetaFunction = () => {
return [
{ title: "评查规则管理 - 中国烟草AI合同及卷宗审核系统" },
{
name: "description",
name: "rules",
content: "评查规则管理模块,包括评查点列表、创建和编辑功能"
}
];
};
export const handle = {
breadcrumb: "评查规则库"
breadcrumb: "评查点列表"
};
/**