封装公共组件,调整样式文件的布局,修改路由页面样式

This commit is contained in:
2025-03-27 19:58:58 +08:00
parent d9b9ce4676
commit 540618b8ca
33 changed files with 2613 additions and 987 deletions
+1 -1
View File
@@ -66,7 +66,7 @@ export function Sidebar({ onToggle, collapsed }: SidebarProps) {
{
id: 'rules-file',
title: '评查文件列表',
path: '/rules/files',
path: '/rules-files',
icon: 'ri-list-check-2'
},
{
+4 -4
View File
@@ -20,11 +20,11 @@ export function Card({
noDivider = true,
}: CardProps) {
return (
<div className={`ant-card ${className} bg-white shadow`}>
<div className={`card ${className}`}>
{(title || extra) && (
<div className={`flex justify-between items-center px-5 py-3 ${noDivider ? '' : 'border-b border-gray-100'}`}>
<div className={`card-header ${noDivider ? '' : 'border-b border-gray-100'}`}>
{title && (
<div className="card-title !mb-0">
<div className="card-title">
{icon && <i className={`${icon} mr-2`}></i>}
<span>{title}</span>
</div>
@@ -36,7 +36,7 @@ export function Card({
)}
</div>
)}
<div className={`ant-card-body ${bodyClassName}`}>
<div className={`card-body ${bodyClassName}`}>
{children}
</div>
</div>
+45
View File
@@ -0,0 +1,45 @@
/**
* 文件图标组件
* 根据文件名后缀显示不同类型的图标
*/
interface FileIconProps {
fileName: string;
className?: string;
size?: 'default' | 'sm' | 'lg';
}
export function FileIcon({ fileName, className = '', size }: FileIconProps) {
const sizeClass = size ? `file-icon-${size}` : '';
let fileType = 'unknown';
let iconClass = 'ri-file-line';
if (fileName.endsWith('.pdf')) {
fileType = 'pdf';
iconClass = 'ri-file-pdf-line';
} else if (fileName.endsWith('.docx') || fileName.endsWith('.doc')) {
fileType = 'doc';
iconClass = 'ri-file-word-2-line';
} else if (fileName.endsWith('.xlsx') || fileName.endsWith('.xls')) {
fileType = 'xls';
iconClass = 'ri-file-excel-2-line';
} else if (fileName.endsWith('.pptx') || fileName.endsWith('.ppt')) {
fileType = 'ppt';
iconClass = 'ri-file-ppt-2-line';
} else if (fileName.endsWith('.zip') || fileName.endsWith('.rar')) {
fileType = 'zip';
iconClass = 'ri-file-zip-line';
} else if (fileName.endsWith('.png') || fileName.endsWith('.jpg') || fileName.endsWith('.jpeg') || fileName.endsWith('.gif')) {
fileType = 'img';
iconClass = 'ri-image-line';
} else if (fileName.endsWith('.txt')) {
fileType = 'txt';
iconClass = 'ri-file-text-line';
}
return (
<span className={`file-icon file-icon-${fileType} ${sizeClass} ${className}`}>
<i className={iconClass}></i>
</span>
);
}
+39
View File
@@ -0,0 +1,39 @@
import { FileType, FILE_TYPE_LABELS } from '~/routes/rules-files';
interface FileTypeTagProps {
fileType: FileType;
className?: string;
size?: 'default' | 'sm' | 'lg';
}
/**
* 文件类型标签组件
* 根据文件类型显示不同样式的标签
*/
export function FileTypeTag({ fileType, className = '', size }: FileTypeTagProps) {
const sizeClass = size ? `file-type-tag-${size}` : '';
const tagClassName = `file-type-tag file-type-tag-${fileType.toLowerCase()} ${sizeClass} file-type-tag-with-icon ${className}`;
// 根据文件类型选择图标
const getFileTypeIcon = () => {
switch (fileType) {
case FileType.CONTRACT:
return <i className="ri-file-list-3-line"></i>;
case FileType.LICENSE:
return <i className="ri-vip-crown-line"></i>;
case FileType.PUNISHMENT:
return <i className="ri-scales-line"></i>;
case FileType.REPORT:
return <i className="ri-file-chart-line"></i>;
default:
return <i className="ri-file-line"></i>;
}
};
return (
<span className={tagClassName}>
{getFileTypeIcon()}
{FILE_TYPE_LABELS[fileType]}
</span>
);
}
+135
View File
@@ -0,0 +1,135 @@
import { SearchBox } from '~/components/ui/SearchBox';
interface FilterOption {
value: string;
label: string;
}
interface FilterSelectProps {
label: string;
name: string;
value: string;
options: FilterOption[];
onChange: (e: React.ChangeEvent<HTMLSelectElement>) => void;
className?: string;
}
/**
* 筛选下拉选择框组件
*/
const FilterSelect = ({ label, name, value, options, onChange, className = '' }: FilterSelectProps) => (
<div className={`filter-item ${className}`}>
<label className="filter-label">{label}</label>
<select
className="form-select filter-control"
name={name}
value={value}
onChange={onChange}
>
<option value=""></option>
{options.map(option => (
<option key={option.value} value={option.value}>{option.label}</option>
))}
</select>
</div>
);
interface FilterPanelProps {
children: React.ReactNode;
className?: string;
actions?: React.ReactNode; // 按钮组,如搜索、重置按钮
noActionDivider?: boolean; // 是否取消按钮组与内容之间的分割线
}
/**
* 通用筛选面板组件
* 用于包裹筛选控件
*
* 使用示例:
* ```tsx
* <FilterPanel
* className="mb-4"
* noActionDivider={true}
* actions={
* <>
* <Button type="default" icon="ri-refresh-line" onClick={handleReset}>重置</Button>
* <Button type="primary" icon="ri-search-line">搜索</Button>
* </>
* }
* >
* <SearchFilter label="名称" placeholder="请输入名称" value={name} onSearch={handleNameSearch} instantSearch={true} />
* <FilterSelect label="状态" name="status" value={status} options={statusOptions} onChange={handleStatusChange} />
* </FilterPanel>
*/
export function FilterPanel({ children, className = '', actions, noActionDivider = false }: FilterPanelProps) {
return (
<div className={`filter-panel ${className}`}>
<div className="filter-list">
{children}
{actions && (
<div className={`filter-actions ${noActionDivider ? 'filter-actions-no-divider' : ''}`}>
{actions}
</div>
)}
</div>
</div>
);
}
interface SearchFilterProps {
label: string;
placeholder: string;
value: string;
onSearch: (value: string) => void;
buttonText?: string;
className?: string;
instantSearch?: boolean; // 是否启用即时搜索(输入内容时立即搜索)
}
/**
* 搜索筛选组件
*
* 使用示例:
* ```tsx
* // 带搜索按钮的搜索框
* <SearchFilter
* label="关键词搜索"
* placeholder="请输入关键词"
* value={keyword}
* onSearch={handleSearch}
* />
*
* // 即时搜索的搜索框(无按钮)
* <SearchFilter
* label="关键词搜索"
* placeholder="请输入关键词"
* value={keyword}
* onSearch={handleSearch}
* instantSearch={true}
* />
*/
export function SearchFilter({
label,
placeholder,
value,
onSearch,
buttonText = "搜索",
className = '',
instantSearch = false
}: SearchFilterProps) {
return (
<div className={`filter-item ${className}`}>
<label className="filter-label">{label}</label>
<SearchBox
placeholder={placeholder}
defaultValue={value}
onSearch={onSearch}
buttonText={instantSearch ? "" : buttonText}
className={`filter-control ${instantSearch ? 'form-input-only' : ''}`}
/>
</div>
);
}
// 导出筛选下拉框组件
export { FilterSelect };
+13 -5
View File
@@ -1,5 +1,4 @@
// app/components/ui/Pagination.tsx
import React from 'react';
interface PaginationProps {
currentPage: number;
@@ -8,6 +7,8 @@ interface PaginationProps {
onChange: (page: number) => void;
onPageSizeChange?: (pageSize: number) => void;
pageSizeOptions?: number[];
showTotal?: boolean;
showPageSizeChanger?: boolean;
}
export function Pagination({
@@ -16,7 +17,9 @@ export function Pagination({
pageSize,
onChange,
onPageSizeChange,
pageSizeOptions = [10, 20, 50]
pageSizeOptions = [10, 20, 50],
showTotal = true,
showPageSizeChanger = true
}: PaginationProps) {
const totalPages = Math.ceil(total / pageSize);
@@ -51,14 +54,17 @@ export function Pagination({
};
return (
<div className="flex justify-between items-center p-4">
{onPageSizeChange && (
<div className="flex flex-wrap justify-between items-center p-4 gap-4">
{showTotal && (
<div className="ant-pagination-options">
<span className="text-sm mr-2"> {total} </span>
<span className="text-sm font-normal"> {total} </span>
{onPageSizeChange && showPageSizeChanger && (
<div className="flex items-center ml-3">
<select
className="form-select ant-pagination-options-size-changer"
value={pageSize}
onChange={(e) => onPageSizeChange(Number(e.target.value))}
aria-label="每页显示条数"
>
{pageSizeOptions.map(size => (
<option key={size} value={size}>{size} /</option>
@@ -66,6 +72,8 @@ export function Pagination({
</select>
</div>
)}
</div>
)}
<div className="ant-pagination">
<button
+14 -7
View File
@@ -1,5 +1,4 @@
import React from 'react';
import { Button } from './Button';
interface SearchBoxProps {
placeholder?: string;
@@ -32,21 +31,29 @@ export function SearchBox({
}
};
const isIconOnly = buttonText === '';
const isFilterControl = className.includes('filter-control');
const searchBoxClass = `search-box ${className} ${isFilterControl ? 'search-box-row' : ''}`;
return (
<form onSubmit={handleSubmit} className={`search-box ${className}`}>
<form onSubmit={handleSubmit} className={searchBoxClass}>
<input
type="text"
id={name}
name={name}
className={className.includes('form-input-only') ? "form-input" : "form-input rounded-r-none"}
className={`form-input ${isFilterControl ? 'flex-1' : ''}`}
placeholder={placeholder}
defaultValue={defaultValue}
onChange={handleChange}
/>
{buttonText && !className.includes('form-input-only') && (
<Button type="primary" icon="ri-search-line" className="rounded-l-none">
{buttonText}
</Button>
{!className.includes('form-input-only') && (
<button
type="submit"
className={`search-button ${isIconOnly ? "icon-only-btn" : ""}`}
>
<i className="ri-search-line"></i>
{buttonText && <span>{buttonText}</span>}
</button>
)}
</form>
);
+69
View File
@@ -0,0 +1,69 @@
import { ReviewStatus, REVIEW_STATUS_LABELS } from '~/routes/rules-files';
interface StatusBadgeProps {
status: ReviewStatus;
issueCount?: number;
className?: string;
size?: 'default' | 'sm' | 'lg';
clickable?: boolean;
onClick?: () => void;
}
/**
* 文件评查状态标签组件
* 根据评查状态显示不同样式的标签
*/
export function StatusBadge({
status,
issueCount = 0,
className = '',
size,
clickable = false,
onClick
}: StatusBadgeProps) {
const statusMap: Record<ReviewStatus, string> = {
[ReviewStatus.PASS]: 'success',
[ReviewStatus.WARNING]: 'warning',
[ReviewStatus.FAIL]: 'error',
[ReviewStatus.PENDING]: 'processing'
};
const badgeType = statusMap[status] || 'default';
const sizeClass = size ? `status-badge-${size}` : '';
const clickableClass = clickable ? 'status-badge-clickable' : '';
// 根据状态选择图标
const getStatusIcon = () => {
switch (status) {
case ReviewStatus.PASS:
return <i className="ri-checkbox-circle-line"></i>;
case ReviewStatus.WARNING:
return <i className="ri-alert-line"></i>;
case ReviewStatus.FAIL:
return <i className="ri-close-circle-line"></i>;
case ReviewStatus.PENDING:
return <i className="ri-time-line"></i>;
default:
return null;
}
};
const handleClick = () => {
if (clickable && onClick) {
onClick();
}
};
return (
<span
className={`status-badge status-badge-${badgeType} status-badge-with-icon ${sizeClass} ${clickableClass} ${className}`}
onClick={handleClick}
role={clickable ? "button" : undefined}
tabIndex={clickable ? 0 : undefined}
>
{getStatusIcon()}
{REVIEW_STATUS_LABELS[status]}
{issueCount > 0 && ` (${issueCount})`}
</span>
);
}
+11 -4
View File
@@ -4,12 +4,16 @@ interface StatusDotProps {
status: StatusType | boolean;
text?: string;
className?: string;
size?: 'default' | 'sm' | 'lg';
pulse?: boolean;
}
export function StatusDot({
status,
text,
className = ''
className = '',
size = 'default',
pulse = false
}: StatusDotProps) {
// 如果status是布尔值,则转换为对应的状态类型
const statusType = typeof status === 'boolean'
@@ -24,10 +28,13 @@ export function StatusDot({
statusType === 'processing' ? '处理中' : '未知'
);
const sizeClass = size !== 'default' ? `status-dot-${size}` : '';
const pulseClass = pulse ? 'status-dot-pulse' : '';
return (
<span className={`inline-flex items-center ${className}`}>
<i className={`status-dot status-dot-${statusType}`}></i>
{statusText}
<span className={`status-dot-with-text ${className}`}>
<span className={`status-dot status-dot-${statusType} ${sizeClass} ${pulseClass}`}></span>
<span className="status-dot-text">{statusText}</span>
</span>
);
}
+4 -4
View File
@@ -39,8 +39,8 @@ export function Table<T extends Record<string, any>>({
};
return (
<div className={`ant-table-wrapper ${className} ${loading ? 'opacity-70' : ''}`}>
<table className="ant-table">
<div className={`ant-table-wrapper ${className} ${loading ? 'ant-table-loading' : ''}`}>
<table className={`ant-table ${bordered ? 'ant-table-bordered' : ''}`}>
<thead>
<tr>
{columns.map((column, index) => (
@@ -86,7 +86,7 @@ export function Table<T extends Record<string, any>>({
<tr>
<td
colSpan={columns.length}
className="py-6 text-center text-gray-500"
className="ant-table-empty py-6 text-center text-gray-500"
>
{emptyText}
</td>
@@ -96,7 +96,7 @@ export function Table<T extends Record<string, any>>({
</table>
{loading && (
<div className="absolute inset-0 flex items-center justify-center bg-white bg-opacity-60 z-10">
<div className="ant-table-loading-indicator">
<div className="flex items-center space-x-2">
<div className="w-5 h-5 border-2 border-primary border-t-transparent rounded-full animate-spin"></div>
<span className="text-gray-600">...</span>
+29 -3
View File
@@ -1,17 +1,43 @@
import React from 'react';
export type TagColor = 'blue' | 'green' | 'cyan' | 'purple' | 'orange' | 'red' | 'default';
export type TagColor = 'blue' | 'green' | 'red' | 'yellow' | 'purple' | 'gray' | 'cyan' | 'orange';
interface TagProps {
color?: TagColor;
children: React.ReactNode;
className?: string;
size?: 'default' | 'sm' | 'lg';
closable?: boolean;
clickable?: boolean;
onClose?: () => void;
}
export function Tag({ color = 'default', children, className = '' }: TagProps) {
export function Tag({
color = 'blue',
children,
className = '',
size,
closable = false,
clickable = false,
onClose
}: TagProps) {
const sizeClass = size ? `tag-${size}` : '';
const closableClass = closable ? 'tag-closable' : '';
const clickableClass = clickable ? 'tag-clickable' : '';
const handleClose = (e: React.MouseEvent) => {
e.stopPropagation();
onClose?.();
};
return (
<span className={`ant-tag ant-tag-${color} ${className}`}>
<span className={`tag tag-${color} ${sizeClass} ${closableClass} ${clickableClass} ${className}`}>
{children}
{closable && (
<span className="tag-close-icon" onClick={handleClose}>
<i className="ri-close-line"></i>
</span>
)}
</span>
);
}
+187 -140
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);
</>
}
}}
noActionDivider={true}
>
<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>
<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,7 +207,8 @@ 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]);
@@ -213,6 +217,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
break;
}
}
}
if (keyword) {
const lowerKeyword = keyword.toLowerCase();
@@ -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,139 +351,53 @@ 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" };
}
};
return (
<div className="p-6 review-files-page">
{/* 页面头部 */}
<div className="flex justify-between items-center mb-4">
// 定义表格列配置
const columns = [
{
title: "文件名称",
key: "fileName",
width: "30%",
render: (_: unknown, file: ReviewFile) => (
<div className="flex items-center">
<h2 className="text-xl font-medium"></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>
</div>
</div>
<Button type="primary" icon="ri-file-upload-line" to="/files/new">
</Button>
</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>
<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>
{/* 文件列表 */}
<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)}
<FileIcon fileName={file.fileName} className="mr-2 text-lg" />
<div>
<div className="font-medium">{file.fileName}</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 && "许可证号:"}
@@ -478,9 +407,14 @@ export default function ReviewFilesList() {
</div>
</div>
</div>
</td>
<td>
<span className={`file-type-badge file-type-${file.fileType}`}>
)
},
{
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>}
@@ -488,26 +422,47 @@ export default function ReviewFilesList() {
{file.fileType === FileType.OTHER && <i className="ri-file-line"></i>}
{FILE_TYPE_LABELS[file.fileType]}
</span>
</td>
<td>
{file.uploadTime.split(' ')[0]}
)
},
{
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>
</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>}
</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>
</td>
<td>
{renderIssues(file.issues)}
</td>
<td>
);
}
},
{
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">
@@ -518,67 +473,105 @@ export default function ReviewFilesList() {
</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>
{/* 分页 */}
{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 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-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-normal text-primary ml-1">{totalCount}</span>
</div>
</div>
<Button type="primary" icon="ri-file-upload-line" to="/files/new">
</Button>
</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"
/>
<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 >
<Table
columns={columns}
dataSource={files}
rowKey="id"
emptyText="暂无文件数据"
className="files-table"
/>
{/* 分页组件 */}
{totalCount > 0 && (
<Pagination
currentPage={currentPage}
total={totalCount}
pageSize={pageSize}
onChange={handlePageChange}
onPageSizeChange={handlePageSizeChange}
showTotal={true}
showPageSizeChanger={true}
pageSizeOptions={[10, 20, 30, 50]}
/>
)}
</Card>
</div>
+418
View File
@@ -0,0 +1,418 @@
import React, { useState } from 'react';
import { redirect, type MetaFunction, type ActionFunctionArgs, type LoaderFunctionArgs } from '@remix-run/node';
import { useLoaderData, useActionData, Form, useSubmit, useNavigate } from '@remix-run/react';
import { Button } from '~/components/ui/Button';
import { Card } from '~/components/ui/Card';
import { Breadcrumb } from '~/components/layout/Breadcrumb';
import type { Rule, RuleType, RulePriority } from '~/models/rule';
export const meta: MetaFunction = () => {
return [
{ title: "中国烟草AI合同及卷宗审核系统 - 评查规则详情" },
{ name: "description", content: "评查规则详情编辑页面" }
];
};
export const handle = {
breadcrumb: '编辑评查点'
};
interface LoaderData {
rule: Rule;
ruleTypes: { label: string; value: RuleType }[];
rulePriorities: { label: string; value: RulePriority }[];
groupOptions: { label: string; value: string }[];
}
export async function loader({ params }: LoaderFunctionArgs) {
const { ruleId } = params;
// 判断是否为新建规则
const isNewRule = ruleId === 'new';
// 模拟数据,实际项目中应从API获取
const rule: Rule = isNewRule ? {
id: '',
name: '',
description: '',
content: '',
type: 'text',
priority: 'medium',
groupId: '',
groupName: '',
isActive: true,
createdAt: '',
updatedAt: ''
} : {
id: ruleId,
name: '许可证编号格式检查',
description: '检查烟草专卖零售许可证编号是否符合"烟零许(年份)序号号"的标准格式',
content: '许可证编号应当符合"烟零许(年份)序号号"的标准格式,如"烟零许(2023)12345号"',
type: 'regex',
priority: 'high',
groupId: '1',
groupName: '专卖许可证规则组',
isActive: true,
createdAt: '2023-10-15 09:30',
updatedAt: '2023-12-10 14:20'
};
// 规则类型选项
const ruleTypes = [
{ label: '文本匹配', value: 'text' },
{ label: '正则表达式', value: 'regex' },
{ label: '数值范围', value: 'range' },
{ label: '日期检查', value: 'date' },
{ label: 'AI智能检查', value: 'ai' }
];
// 规则优先级选项
const rulePriorities = [
{ label: '低', value: 'low' },
{ label: '中', value: 'medium' },
{ label: '高', value: 'high' },
{ label: '关键', value: 'critical' }
];
// 规则组选项
const groupOptions = [
{ label: '专卖许可证规则组', value: '1' },
{ label: '合同协议规则组', value: '2' },
{ label: '财务票据规则组', value: '3' },
{ label: '采购订单规则组', value: '4' },
{ label: '销售报表规则组', value: '5' }
];
return Response.json({
rule,
ruleTypes,
rulePriorities,
groupOptions
});
}
interface ActionData {
success?: boolean;
errors?: {
name?: string;
description?: string;
content?: string;
type?: string;
priority?: string;
groupId?: string;
general?: string;
};
}
export async function action({ request, params }: ActionFunctionArgs) {
const { ruleId } = params;
const formData = await request.formData();
const isNewRule = ruleId === 'new';
// 获取表单数据
const name = formData.get('name')?.toString() || '';
const description = formData.get('description')?.toString() || '';
const content = formData.get('content')?.toString() || '';
const type = formData.get('type')?.toString() || '';
const priority = formData.get('priority')?.toString() || '';
const groupId = formData.get('groupId')?.toString() || '';
const isActive = formData.get('isActive') === 'true';
// 表单验证
const errors: ActionData['errors'] = {};
if (!name.trim()) {
errors.name = '规则名称不能为空';
}
if (!content.trim()) {
errors.content = '规则内容不能为空';
}
if (!type) {
errors.type = '必须选择规则类型';
}
if (!priority) {
errors.priority = '必须选择规则优先级';
}
if (!groupId) {
errors.groupId = '必须选择规则所属组';
}
if (Object.keys(errors).length > 0) {
return Response.json({ errors });
}
// 模拟API保存操作,实际项目中应调用API
try {
// 在这里调用API进行保存
console.log('保存规则:', {
id: isNewRule ? 'new-id' : ruleId,
name,
description,
content,
type,
priority,
groupId,
isActive
});
// 成功后重定向到规则列表页
return redirect('/rules');
} catch (error) {
return Response.json({
errors: {
general: '保存规则失败,请重试'
}
});
}
}
export default function RuleDetail() {
const { rule, ruleTypes, rulePriorities, groupOptions } = useLoaderData<typeof loader>();
const actionData = useActionData<typeof action>();
const navigate = useNavigate();
const submit = useSubmit();
const [formData, setFormData] = useState({
name: rule.name,
description: rule.description,
content: rule.content,
type: rule.type,
priority: rule.priority,
groupId: rule.groupId,
isActive: rule.isActive
});
const isNewRule = !rule.id;
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
};
const handleSwitchChange = (name: string, checked: boolean) => {
setFormData(prev => ({ ...prev, [name]: checked }));
};
const handleCancel = () => {
navigate('/rules');
};
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
// 使用useSubmit提交表单
const formElement = e.currentTarget;
submit(formElement, { method: 'post' });
};
return (
<div>
<Breadcrumb
items={[
{ title: '评查规则', to: '/rules' },
{ title: isNewRule ? '新增规则' : '编辑规则', to: `/rules/${rule.id}` }
]}
/>
<div className="flex justify-between items-center mb-4">
<h2 className="text-xl font-medium">{isNewRule ? '新增评查规则' : '编辑评查规则'}</h2>
</div>
<Card>
<Form method="post" onSubmit={handleSubmit}>
{actionData?.errors?.general && (
<div className="error-message mb-4">
<i className="ri-error-warning-line mr-1"></i>
{actionData.errors.general}
</div>
)}
<div className="form-section mb-6">
<h3 className="form-section-title"></h3>
<div className="form-row">
<div className="form-group col-span-6">
<label htmlFor="name" className="form-label required"></label>
<input
type="text"
id="name"
name="name"
className={`form-input ${actionData?.errors?.name ? 'error' : ''}`}
value={formData.name}
onChange={handleChange}
required
/>
{actionData?.errors?.name && (
<div className="form-error">{actionData.errors.name}</div>
)}
</div>
<div className="form-group col-span-6">
<label htmlFor="groupId" className="form-label required"></label>
<select
id="groupId"
name="groupId"
className={`form-select ${actionData?.errors?.groupId ? 'error' : ''}`}
value={formData.groupId}
onChange={handleChange}
required
>
<option value=""></option>
{groupOptions.map((option: { value: string; label: string }) => (
<option key={option.value} value={option.value}>{option.label}</option>
))}
</select>
{actionData?.errors?.groupId && (
<div className="form-error">{actionData.errors.groupId}</div>
)}
</div>
</div>
<div className="form-row">
<div className="form-group col-span-12">
<label htmlFor="description" className="form-label"></label>
<textarea
id="description"
name="description"
className="form-textarea"
rows={3}
value={formData.description}
onChange={handleChange}
></textarea>
</div>
</div>
</div>
<div className="form-section mb-6">
<h3 className="form-section-title"></h3>
<div className="form-row">
<div className="form-group col-span-4">
<label htmlFor="type" className="form-label required"></label>
<select
id="type"
name="type"
className={`form-select ${actionData?.errors?.type ? 'error' : ''}`}
value={formData.type}
onChange={handleChange}
required
>
<option value=""></option>
{ruleTypes.map((option: { value: string; label: string }) => (
<option key={option.value} value={option.value}>{option.label}</option>
))}
</select>
{actionData?.errors?.type && (
<div className="form-error">{actionData.errors.type}</div>
)}
</div>
<div className="form-group col-span-4">
<label htmlFor="priority" className="form-label required"></label>
<select
id="priority"
name="priority"
className={`form-select ${actionData?.errors?.priority ? 'error' : ''}`}
value={formData.priority}
onChange={handleChange}
required
>
<option value=""></option>
{rulePriorities.map((option: { value: string; label: string }) => (
<option key={option.value} value={option.value}>{option.label}</option>
))}
</select>
{actionData?.errors?.priority && (
<div className="form-error">{actionData.errors.priority}</div>
)}
</div>
<div className="form-group col-span-4">
<label htmlFor="isActive" className="form-label"></label>
<div className="flex items-center h-10 mt-1">
<label className="switch" aria-label="切换规则状态">
<input
type="checkbox"
name="isActive"
checked={formData.isActive}
onChange={(e) => handleSwitchChange('isActive', e.target.checked)}
/>
<span className="slider round"></span>
</label>
<input type="hidden" name="isActive" value={formData.isActive ? 'true' : 'false'} />
<span className="ml-2">{formData.isActive ? '启用' : '禁用'}</span>
</div>
</div>
</div>
<div className="form-row">
<div className="form-group col-span-12">
<label htmlFor="content" className="form-label required"></label>
<textarea
id="content"
name="content"
className={`form-textarea code-editor ${actionData?.errors?.content ? 'error' : ''}`}
rows={8}
value={formData.content}
onChange={handleChange}
required
></textarea>
{actionData?.errors?.content && (
<div className="form-error">{actionData.errors.content}</div>
)}
{formData.type === 'regex' && (
<div className="text-xs text-gray-500 mt-1">
<i className="ri-information-line mr-1"></i>
</div>
)}
{formData.type === 'ai' && (
<div className="text-xs text-gray-500 mt-1">
<i className="ri-information-line mr-1"></i>
使AI将自动理解并执行检查
</div>
)}
</div>
</div>
</div>
<div className="form-section mb-6">
<h3 className="form-section-title"></h3>
<div className="p-4 bg-gray-50 rounded-md">
<div className="mb-4">
<label htmlFor="testContent" className="form-label"></label>
<textarea
id="testContent"
className="form-textarea"
rows={4}
placeholder="粘贴待测试的文本内容..."
></textarea>
</div>
<div className="flex justify-end">
<Button type="default">
<i className="ri-test-tube-line mr-1"></i>
</Button>
</div>
</div>
</div>
<div className="flex justify-end space-x-2">
<Button type="default" onClick={handleCancel}>
</Button>
<Button type="primary">
{isNewRule ? '创建规则' : '保存修改'}
</Button>
</div>
</Form>
</Card>
</div>
);
}
+151 -184
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"
<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}
>
<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"
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}
>
<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"
className="mr-3 w-80"
/>
<FilterSelect
label="状态"
name="isActive"
value={searchParams.get('isActive') || ''}
options={[
{ value: "true", label: "启用" },
{ value: "false", label: "禁用" }
]}
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}
className="mr-3 w-80"
/>
</div>
</div>
</Card>
{/* 评查点列表 */}
<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>
+1 -1
View File
@@ -16,7 +16,7 @@ export const meta: MetaFunction = () => {
};
export const handle = {
breadcrumb: "评查规则库"
breadcrumb: "评查点列表"
};
/**
+1 -1
View File
@@ -26,7 +26,7 @@
/* 卡片内容 */
.card-body {
@apply p-5;
@apply p-1;
}
/* 卡片底部 */
+50
View File
@@ -0,0 +1,50 @@
/**
* 文件图标组件样式
*/
/* 文件图标容器 */
.file-icon {
@apply inline-flex items-center justify-center w-10 h-10 rounded-md text-xl;
}
/* 文件图标类型 */
.file-icon-doc {
@apply bg-blue-100 text-blue-600;
}
.file-icon-pdf {
@apply bg-red-100 text-red-600;
}
.file-icon-xls {
@apply bg-green-100 text-green-600;
}
.file-icon-ppt {
@apply bg-orange-100 text-orange-600;
}
.file-icon-zip {
@apply bg-purple-100 text-purple-600;
}
.file-icon-img {
@apply bg-pink-100 text-pink-600;
}
.file-icon-txt {
@apply bg-gray-100 text-gray-600;
}
.file-icon-unknown {
@apply bg-gray-100 text-gray-600;
}
/* 文件图标尺寸 */
.file-icon-sm {
@apply w-8 h-8 text-base;
}
.file-icon-lg {
@apply w-12 h-12 text-2xl;
}
+80
View File
@@ -0,0 +1,80 @@
/**
* 文件类型标签组件样式
*/
/* 文件类型标签基础样式 */
.file-type-tag {
@apply inline-flex items-center px-2 py-1 rounded text-xs font-medium;
}
/* 文件类型颜色 */
.file-type-tag-doc {
@apply bg-blue-100 text-blue-800;
}
.file-type-tag-pdf {
@apply bg-red-100 text-red-800;
}
.file-type-tag-xls {
@apply bg-green-100 text-green-800;
}
.file-type-tag-ppt {
@apply bg-orange-100 text-orange-800;
}
.file-type-tag-zip {
@apply bg-purple-100 text-purple-800;
}
.file-type-tag-img {
@apply bg-pink-100 text-pink-800;
}
.file-type-tag-txt {
@apply bg-gray-100 text-gray-800;
}
.file-type-tag-unknown {
@apply bg-gray-100 text-gray-800;
}
/* 特定业务文件类型 */
.file-type-tag-contract {
@apply bg-blue-100 text-blue-800;
}
.file-type-tag-license {
@apply bg-green-100 text-green-800;
}
.file-type-tag-punishment {
@apply bg-orange-100 text-orange-800;
}
.file-type-tag-report {
@apply bg-cyan-100 text-cyan-800;
}
.file-type-tag-other {
@apply bg-purple-100 text-purple-800;
}
/* 文件类型尺寸 */
.file-type-tag-sm {
@apply px-1.5 py-0 text-xs;
}
.file-type-tag-lg {
@apply px-2.5 py-1 text-sm;
}
/* 带图标的文件类型标签 */
.file-type-tag-with-icon {
@apply pl-1.5;
}
.file-type-tag i {
@apply mr-1 text-sm;
}
+57
View File
@@ -0,0 +1,57 @@
/**
* 筛选面板组件样式
*/
/* 筛选面板容器 */
.filter-panel {
@apply bg-white rounded-lg border border-gray-200 shadow-sm p-4 mb-5;
}
/* 筛选条件列表 */
.filter-list {
@apply flex flex-wrap items-end gap-3;
}
/* 筛选项 */
.filter-item {
@apply mb-0;
}
/* 筛选标签 */
.filter-label {
@apply block text-sm font-medium mb-1 text-gray-700;
}
/* 筛选控件 */
.filter-control {
@apply w-full;
}
/* 筛选操作按钮区域 */
.filter-actions {
@apply flex justify-end items-center pt-4 mt-4 border-t border-gray-100 space-x-3;
}
/* 无分割线的按钮区域 */
.filter-actions-no-divider {
@apply border-t-0 pt-0 mt-2;
}
/* 收起/展开状态 */
.filter-panel-collapsed {
@apply max-h-[120px] overflow-hidden relative;
}
.filter-panel-collapsed::after {
content: '';
@apply absolute bottom-0 left-0 right-0 h-12 bg-gradient-to-t from-white to-transparent;
}
/* 收起/展开切换按钮 */
.filter-toggle {
@apply text-sm text-[#00684a] cursor-pointer hover:text-[#005a3f] flex items-center;
}
.filter-toggle i {
@apply ml-1;
}
+60
View File
@@ -0,0 +1,60 @@
/**
* 分页组件样式
*/
/* 分页容器 */
.ant-pagination {
@apply flex items-center space-x-1;
}
/* 分页项 */
.ant-pagination-item {
@apply flex items-center justify-center min-w-[32px] h-8 px-3
border border-gray-200 rounded-md text-sm text-gray-700
transition-all duration-200 cursor-pointer;
}
.ant-pagination-item:hover {
@apply border-[#00684a] text-[#00684a];
/* @apply border-[#30184a] text-[#30184a]; */
}
.ant-pagination-item-active {
@apply border-[#00684a] bg-[#00684a] text-white font-medium;
}
.ant-pagination-item-active:hover {
@apply text-white;
}
/* 上一页/下一页按钮 */
.ant-pagination-prev,
.ant-pagination-next {
@apply flex items-center justify-center w-8 h-8
border border-gray-200 rounded-md text-gray-600
transition-all duration-200;
}
.ant-pagination-prev:hover,
.ant-pagination-next:hover {
@apply border-[#00684a] text-[#00684a];
}
/* 禁用状态 */
.ant-pagination-disabled {
@apply opacity-50 cursor-not-allowed;
}
.ant-pagination-disabled:hover {
@apply border-gray-200 text-gray-600;
}
/* 显示总数和每页显示条数 */
.ant-pagination-options {
@apply flex items-center text-sm text-gray-500 mr-4;
}
.ant-pagination-options-size-changer {
@apply ml-2 px-2 py-1 border border-gray-200 rounded text-sm
focus:outline-none focus:ring-1 focus:ring-[#00684a] focus:border-[#00684a];
}
+80
View File
@@ -0,0 +1,80 @@
/**
* 搜索框组件样式
*/
/* 搜索框容器 */
.search-box {
@apply relative;
}
/* 搜索框与按钮并排显示 */
.search-box-row {
@apply flex items-center;
}
.search-box-row .form-input {
@apply rounded-r-none;
}
.search-box-row .search-button {
@apply rounded-l-none h-full flex items-center;
}
/* 搜索输入框 */
.form-input {
@apply w-full py-2 px-4 border border-gray-200 rounded-md text-sm
focus:outline-none focus:ring-1 focus:ring-[#00684a] focus:border-[#00684a]
placeholder-gray-400 transition-all duration-200;
}
/* 搜索按钮 */
.search-button {
@apply bg-[#00684a] text-white px-3 py-2 rounded-md border border-[#00684a];
}
.search-button:hover {
@apply bg-[#005a3f] border-[#005a3f];
}
.search-button i {
@apply mr-1;
}
/* 仅图标按钮样式 */
.icon-only-btn {
@apply px-2;
}
.icon-only-btn i {
@apply mr-0;
}
/* 搜索框大小 */
.search-box-sm .form-input {
@apply py-1.5 text-sm;
}
.search-box-lg .form-input {
@apply py-2.5 text-base;
}
/* 搜索图标 */
.search-box-icon {
@apply absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400;
}
/* 清除按钮 */
.search-box-clear {
@apply absolute right-3 top-1/2 transform -translate-y-1/2
text-gray-400 hover:text-gray-600 cursor-pointer transition-colors duration-200;
}
/* 带边框的搜索框 */
.search-box-bordered {
@apply shadow-sm;
}
/* 搜索框禁用状态 */
.form-input:disabled {
@apply bg-gray-100 cursor-not-allowed opacity-70;
}
+72
View File
@@ -0,0 +1,72 @@
/**
* 状态徽章组件样式
*/
/* 状态徽章基础样式 */
.status-badge {
@apply inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium;
}
/* 状态徽章尺寸 */
.status-badge-sm {
@apply px-2 py-0.5 text-xs;
}
.status-badge-lg {
@apply px-3 py-1 text-sm;
}
/* 状态徽章类型 */
.status-badge-success {
@apply bg-green-100 text-green-800;
}
.status-badge-processing {
@apply bg-blue-100 text-blue-800;
}
.status-badge-warning {
@apply bg-yellow-100 text-yellow-800;
}
.status-badge-error {
@apply bg-red-100 text-red-800;
}
.status-badge-default {
@apply bg-gray-100 text-gray-800;
}
/* 带图标的状态徽章 */
.status-badge-with-icon {
@apply pl-1.5;
}
.status-badge i {
@apply mr-1;
}
/* 可点击的状态徽章 */
.status-badge-clickable {
@apply cursor-pointer transition-colors duration-200;
}
.status-badge-clickable.status-badge-success:hover {
@apply bg-green-200;
}
.status-badge-clickable.status-badge-processing:hover {
@apply bg-blue-200;
}
.status-badge-clickable.status-badge-warning:hover {
@apply bg-yellow-200;
}
.status-badge-clickable.status-badge-error:hover {
@apply bg-red-200;
}
.status-badge-clickable.status-badge-default:hover {
@apply bg-gray-200;
}
+83
View File
@@ -0,0 +1,83 @@
/**
* 状态点组件样式
*/
/* 状态点基础样式 */
.status-dot {
@apply inline-block w-2 h-2 rounded-full;
}
/* 状态点类型 */
.status-dot-success {
@apply bg-green-500;
}
.status-dot-processing {
@apply bg-blue-500;
}
.status-dot-warning {
@apply bg-yellow-500;
}
.status-dot-error {
@apply bg-red-500;
}
.status-dot-default {
@apply bg-gray-400;
}
/* 状态点尺寸 */
.status-dot-sm {
@apply w-1.5 h-1.5;
}
.status-dot-lg {
@apply w-3 h-3;
}
/* 带脉冲动画的状态点 */
.status-dot-pulse {
@apply relative;
}
.status-dot-pulse::after {
content: '';
@apply absolute w-full h-full rounded-full -left-1 -top-1 animate-ping;
}
.status-dot-pulse.status-dot-success::after {
@apply bg-green-400 opacity-60;
}
.status-dot-pulse.status-dot-processing::after {
@apply bg-blue-400 opacity-60;
}
.status-dot-pulse.status-dot-warning::after {
@apply bg-yellow-400 opacity-60;
}
.status-dot-pulse.status-dot-error::after {
@apply bg-red-400 opacity-60;
}
/* 带文本的状态点 */
.status-dot-with-text {
@apply flex items-center justify-center;
}
.status-dot-text {
@apply ml-1.5 text-sm;
color: inherit;
}
/* 状态文本颜色 */
.status-dot-success + .status-dot-text {
@apply text-green-600;
}
.status-dot-default + .status-dot-text {
@apply text-gray-500;
}
+61
View File
@@ -2,6 +2,67 @@
* 表格组件样式
*/
/* 表格容器 */
.ant-table-wrapper {
@apply w-full overflow-x-auto relative;
}
/* 表格 */
.ant-table {
@apply w-full border-collapse text-sm;
}
/* 表头 */
.ant-table thead th {
@apply bg-gray-50 font-medium py-3 px-4 text-left text-gray-700 border-b border-gray-200;
}
/* 表格内容 */
.ant-table tbody td {
@apply py-3 px-4 border-b border-gray-100;
}
/* 表格行 */
.ant-table tbody tr {
@apply bg-white hover:bg-gray-50 transition-colors duration-150;
}
/* 空状态 */
.ant-table-empty {
@apply py-12 text-center text-gray-500;
}
/* 带边框的表格 */
.ant-table-bordered,
.ant-table-bordered .ant-table thead th,
.ant-table-bordered .ant-table tbody td {
@apply border border-gray-200;
}
/* 表格加载状态 */
.ant-table-loading {
@apply relative opacity-60;
}
.ant-table-loading-indicator {
@apply absolute inset-0 flex items-center justify-center bg-white bg-opacity-70;
}
/* 表格行选中状态 */
.ant-table tr.selected {
@apply bg-[rgba(0,104,74,0.05)];
}
/* 表格排序图标 */
.ant-table-column-sorter {
@apply ml-1 text-gray-400 inline-flex flex-col;
}
.ant-table-column-sorter-up.active,
.ant-table-column-sorter-down.active {
@apply text-[#00684a];
}
@layer components {
/* 基础表格 */
.table-container {
+65
View File
@@ -0,0 +1,65 @@
/**
* 标签组件样式
*/
/* 标签基础样式 */
.tag {
@apply inline-flex items-center px-2.5 py-0.5 rounded-md text-xs font-medium;
}
/* 标签类型 */
.tag-blue {
@apply bg-blue-100 text-blue-800;
}
.tag-green {
@apply bg-green-100 text-green-800;
}
.tag-red {
@apply bg-red-100 text-red-800;
}
.tag-yellow {
@apply bg-yellow-100 text-yellow-800;
}
.tag-purple {
@apply bg-purple-100 text-purple-800;
}
.tag-gray {
@apply bg-gray-100 text-gray-800;
}
/* 添加新的颜色 */
.tag-cyan {
@apply bg-cyan-100 text-cyan-800;
}
.tag-orange {
@apply bg-orange-100 text-orange-800;
}
/* 标签尺寸 */
.tag-sm {
@apply px-2 py-0 text-xs;
}
.tag-lg {
@apply px-3 py-1 text-sm;
}
/* 可关闭的标签 */
.tag-closable {
@apply pr-1;
}
.tag-close-icon {
@apply ml-1 cursor-pointer text-opacity-70 hover:text-opacity-100 transition-opacity duration-200;
}
/* 可点击的标签 */
.tag-clickable {
@apply cursor-pointer hover:opacity-80 transition-opacity duration-200;
}
+17
View File
@@ -3,6 +3,23 @@
* 包含应用所有样式
*/
/* 导入组件样式 */
@import './components/button.css';
@import './components/card.css';
@import './components/form.css';
@import './components/navigation.css';
@import './components/table.css';
@import './components/badge.css';
@import './components/pagination.css';
@import './components/search-box.css';
@import './components/filter-panel.css';
@import './components/file-icon.css';
@import './components/status-badge.css';
@import './components/file-type-tag.css';
@import './components/status-dot.css';
@import './components/tag.css';
/* @import './components/modal.css'; */
/* Tailwind 基础指令 */
@tailwind base;
@tailwind components;
+2 -2
View File
@@ -96,7 +96,7 @@
@apply flex items-center;
}
/* 状态标签 */
/* 状态标签 - 与status-badge.css重复,已注释
.status-badge {
@apply inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium;
}
@@ -111,7 +111,7 @@
.status-badge.status-error {
@apply bg-[rgba(245,34,45,0.1)] text-[#f5222d];
}
} */
/* 卡片样式 */
.dashboard-card {
+55 -110
View File
@@ -93,23 +93,6 @@
color: var(--color-primary);
}
/* 状态点样式 */
.rule-groups-page .status-dot {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
margin-right: 6px;
}
.rule-groups-page .status-success {
background-color: #52c41a;
}
.rule-groups-page .status-error {
background-color: #f5222d;
}
/* 表单样式 */
.rule-groups-page .form-label {
display: block;
@@ -136,89 +119,6 @@
outline: none;
}
/* 卡片样式 */
.rule-groups-page .ant-card {
background-color: white;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
border: 1px solid #e9ecef;
}
.rule-groups-page .ant-card-body {
padding: 16px;
}
/* 表格样式 */
.rule-groups-page .ant-table {
width: 100%;
border-collapse: collapse;
}
.rule-groups-page .ant-table th,
.rule-groups-page .ant-table td {
padding: 12px 16px;
text-align: left;
border-bottom: 1px solid #e9ecef;
}
.rule-groups-page .ant-table th {
background-color: #f8f9fa;
font-weight: 400; /* 减少文字粗细度 */
color: #495057;
font-size: 14px;
}
/* 分页样式 */
.rule-groups-page .ant-pagination {
display: flex;
align-items: center;
gap: 8px;
}
.rule-groups-page .ant-pagination-item {
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid #dee2e6;
border-radius: 4px;
background-color: white;
cursor: pointer;
transition: all 0.2s;
}
.rule-groups-page .ant-pagination-item:hover {
border-color: #00684a;
color: #00684a;
}
.rule-groups-page .ant-pagination-item-active {
background-color: #00684a;
border-color: #00684a;
color: white;
}
.rule-groups-page .ant-pagination-prev,
.rule-groups-page .ant-pagination-next {
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid #dee2e6;
border-radius: 4px;
background-color: white;
cursor: pointer;
transition: all 0.2s;
}
.rule-groups-page .ant-pagination-prev:disabled,
.rule-groups-page .ant-pagination-next:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* 特定链接样式 */
.rule-groups-page .badge {
display: inline-flex;
@@ -253,12 +153,12 @@
color: #f5222d;
}
/* 行操作按钮 */
/* 文本按钮样式 */
.rule-groups-page .ant-btn-text {
background: transparent;
margin: 10px;
background-color: transparent;
border: none;
padding: 4px 0;
padding: 4px 8px;
font-size: 14px;
cursor: pointer;
transition: color 0.2s;
}
@@ -268,7 +168,8 @@
}
.rule-groups-page .ant-btn-text.text-primary:hover {
color: #005a3f;
color: #00684a;
text-decoration: underline;
}
.rule-groups-page .ant-btn-text.text-error {
@@ -276,21 +177,21 @@
}
.rule-groups-page .ant-btn-text.text-error:hover {
color: #cf1f29;
color: #f5222d;
text-decoration: underline;
}
/* 响应式调整 */
@media (max-width: 768px) {
.rule-groups-page .flex-wrap {
flex-direction: column;
flex-wrap: wrap;
}
.rule-groups-page .flex-1 {
width: 100%;
flex: 1 1 100%;
}
.rule-groups-page .ant-table {
font-size: 14px;
font-size: 12px;
}
}
@@ -317,3 +218,47 @@
width: 100%;
}
/* 表格组件样式兼容 - 确保Table组件渲染的表格保持树形结构样式 */
.rule-groups-page .tree-table tr:nth-child(odd) {
background-color: rgba(0, 104, 74, 0.02);
}
.rule-groups-page .tree-table tr:hover td {
background-color: rgba(0, 104, 74, 0.05);
}
/* 确保父子关系行高一致 */
.rule-groups-page .tree-table tr td {
padding: 12px 16px;
vertical-align: middle;
}
/* 父行背景色 */
.rule-groups-page .tree-table tr:has(.parent-badge) {
background-color: #f9f9f9;
}
/* 确保链接样式一致 */
.rule-groups-page .tree-table a {
/* color: inherit; */
text-decoration: none;
}
.rule-groups-page .tree-table a:hover {
text-decoration: underline;
}
/* 搜索筛选面板样式调整 */
.rule-groups-page .filter-panel {
padding: 16px;
}
.rule-groups-page .filter-item {
margin-bottom: 0;
}
.rule-groups-page .filter-actions {
margin-top: 8px;
padding-top: 8px;
}
+479
View File
@@ -0,0 +1,479 @@
/* 评查文件列表页面样式 */
.review-files-page {
/* 所有样式都包含在此命名空间内 */
}
/* 筛选区域 - 与filter-panel.css重复,已注释
.review-files-page .filter-panel {
background-color: white;
border-radius: 6px;
border: 1px solid var(--color-gray-200);
padding: 12px 16px;
margin-bottom: 16px;
display: flex;
flex-wrap: wrap;
align-items: flex-end;
gap: 12px;
} */
.review-files-page .form-label {
display: block;
margin-bottom: 4px;
font-size: 14px;
font-weight: 400;
color: var(--color-gray-800);
}
/* 搜索框样式 - 与search-box.css重复,已注释
.review-files-page .search-box {
display: flex;
align-items: stretch;
width: 100%;
border-radius: 4px;
overflow: visible;
position: relative;
}
.review-files-page .search-box .form-input {
flex: 1;
font-size: 14px;
padding: 8px 12px;
border: 1px solid #d9d9d9;
border-right: none;
outline: none;
background-color: transparent;
height: 38px;
border-radius: 4px 0 0 4px;
box-shadow: none;
transition: all 0.3s;
}
.review-files-page .search-box .form-input:focus {
outline: none;
border-color: var(--color-primary);
box-shadow: 0 0 0 2px rgba(0, 104, 74, 0.1);
} */
.review-files-page .search-box .search-button {
border-top-left-radius: 0 !important;
border-bottom-left-radius: 0 !important;
border-top-right-radius: 4px !important;
border-bottom-right-radius: 4px !important;
margin: 0 !important;
border: 1px solid var(--color-primary) !important;
height: 38px !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
min-width: 40px !important;
background-color: var(--color-primary) !important;
color: white !important;
cursor: pointer !important;
padding: 0 16px !important;
transition: all 0.3s !important;
outline: none !important;
}
.review-files-page .search-box .search-button:hover {
background-color: var(--color-primary-hover) !important;
}
.review-files-page .search-box .search-button:focus {
outline: none !important;
box-shadow: 0 0 0 2px rgba(0, 104, 74, 0.2) !important;
}
.review-files-page .search-box .search-button i {
font-size: 16px !important;
margin-right: 0 !important;
}
.review-files-page .search-box .search-button span {
margin-left: 4px !important;
}
/* 仅图标按钮样式 */
.review-files-page .search-box .icon-only-btn {
padding: 0 12px !important;
min-width: 40px !important;
}
/* 确保旧的按钮样式不影响搜索按钮 */
.review-files-page .search-box .ant-btn {
line-height: normal !important;
}
/* 表格样式 - 与table.css重复,已注释
.review-files-page .ant-table {
width: 100%;
border-collapse: separate;
border-spacing: 0;
}
.review-files-page .ant-table th {
background-color: #fafafa;
font-weight: 400;
padding: 16px;
text-align: left;
border-bottom: 1px solid #f0f0f0;
color: rgba(0, 0, 0, 0.85);
font-size: 14px;
}
.review-files-page .ant-table td {
padding: 16px;
border-bottom: 1px solid #f0f0f0;
text-align: left;
}
.review-files-page .ant-table tr:hover td {
background-color: #f5f5f5;
} */
/* 文件类型徽章 - 与file-type-tag.css重复,已注释
.review-files-page .file-type-badge {
display: inline-flex;
align-items: center;
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 400;
}
.review-files-page .file-type-badge i {
margin-right: 4px;
font-size: 14px;
} */
.review-files-page .file-type-contract {
background-color: #e6f7ff;
color: #1890ff;
}
.review-files-page .file-type-license {
background-color: #f6ffed;
color: #52c41a;
}
.review-files-page .file-type-punishment {
background-color: #fff7e6;
color: #fa8c16;
}
.review-files-page .file-type-report {
background-color: #e6fffb;
color: #13c2c2;
}
.review-files-page .file-type-other {
background-color: #f9f0ff;
color: #722ed1;
}
/* 状态徽章 - 与status-badge.css重复,已注释
.review-files-page .status-badge {
display: inline-flex;
align-items: center;
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 400;
}
.review-files-page .status-badge i {
margin-right: 4px;
} */
.review-files-page .status-pass {
background-color: #f6ffed;
color: var(--color-success);
}
.review-files-page .status-warning {
background-color: #fffbe6;
color: var(--color-warning);
}
.review-files-page .status-fail {
background-color: #fff1f0;
color: var(--color-error);
}
.review-files-page .status-pending {
background-color: #f9f0ff;
color: #722ed1;
}
/* 严重程度指示器 */
.review-files-page .severity-indicator {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
margin-right: 8px;
vertical-align: middle;
}
.review-files-page .severity-info {
background-color: #1890ff;
}
.review-files-page .severity-warning {
background-color: #faad14;
}
.review-files-page .severity-error {
background-color: #f5222d;
}
.review-files-page .severity-critical {
background-color: #722ed1;
}
/* 分页样式 - 与pagination.css重复,已注释
.review-files-page .pagination {
display: flex;
justify-content: flex-end;
margin-top: 16px;
padding: 16px;
}
.review-files-page .pagination-item {
min-width: 32px;
height: 32px;
margin-right: 8px;
display: inline-flex;
justify-content: center;
align-items: center;
border: 1px solid #d9d9d9;
border-radius: 4px;
font-size: 14px;
} */
/* 更新分页样式,与rules._index.tsx保持一致 */
.review-files-page .ant-pagination {
display: flex;
align-items: center;
margin-top: 16px;
padding: 16px;
justify-content: flex-end;
}
.review-files-page .ant-pagination-right {
display: flex;
align-items: center;
gap: 4px;
}
.review-files-page .ant-pagination-item {
min-width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid #dee2e6;
border-radius: 4px;
background-color: white;
cursor: pointer;
transition: all 0.2s;
padding: 0 8px;
margin-right: 4px;
}
.review-files-page .ant-pagination-item-active {
background-color: var(--color-primary);
border-color: var(--color-primary);
color: white;
}
.review-files-page .ant-pagination-disabled {
opacity: 0.5;
cursor: not-allowed;
}
.review-files-page .ant-pagination-prev,
.review-files-page .ant-pagination-next {
min-width: 32px;
height: 32px;
}
.review-files-page .ant-pagination-options {
display: flex;
align-items: center;
margin-right: auto;
min-width: 200px;
padding-right: 12px;
}
.review-files-page .ant-pagination-options span.text-sm {
white-space: nowrap;
flex-shrink: 0;
color: var(--color-gray-700);
margin-right: 12px;
}
.review-files-page .ant-pagination-options-size-changer {
margin-left: 0;
border: 1px solid var(--color-gray-300);
border-radius: 4px;
height: 32px;
padding: 0 8px;
min-width: 110px;
outline: none;
background-color: white;
color: var(--color-gray-800);
transition: all 0.2s;
font-size: 14px;
}
.review-files-page .ant-pagination-options-size-changer:hover,
.review-files-page .ant-pagination-options-size-changer:focus {
border-color: var(--color-primary);
outline: none;
box-shadow: 0 0 0 2px rgba(0, 104, 74, 0.1);
}
/* 表单组件样式 */
.review-files-page .form-select {
width: 100%;
height: 38px;
padding: 8px 12px;
border: 1px solid #d9d9d9;
border-radius: 4px;
font-size: 14px;
transition: all 0.2s;
background-color: white;
color: rgba(0, 0, 0, 0.85);
}
.review-files-page .form-select:focus {
border-color: var(--color-primary);
box-shadow: 0 0 0 2px rgba(0, 104, 74, 0.2);
outline: none;
}
/* 内容卡片样式 */
.review-files-page .ant-card {
background-color: white;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
border: 1px solid #e9ecef;
margin-bottom: 16px;
}
.review-files-page .ant-card-body {
padding: 16px;
}
/* 文本颜色辅助类 */
.review-files-page .text-success {
color: var(--color-success);
}
.review-files-page .text-warning {
color: var(--color-warning);
}
.review-files-page .text-error {
color: var(--color-error);
}
.review-files-page .text-secondary {
color: var(--color-gray-600);
}
/* 错误容器样式 */
.review-files-page .error-container {
text-align: center;
padding: 48px;
background-color: #fff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
max-width: 500px;
margin: 48px auto;
}
/* 按钮尺寸样式 */
.review-files-page .ant-btn-sm {
padding: 4px 8px;
font-size: 12px;
height: 38px;
line-height: 1;
background-color: white;
border: 1px solid #e9ecef;
transition: all 0.3s;
}
/* 操作按钮样式 - 根据需求更新 */
/* 默认按钮样式 - 非主要按钮 */
.review-files-page .ant-btn-default {
background-color: white;
border: 1px solid #e9ecef;
color: rgba(0, 0, 0, 0.85);
}
.review-files-page .ant-btn-default:hover {
border-color: var(--color-primary);
color: var(--color-primary);
}
/* 主要按钮样式 - 确认按钮 */
.review-files-page .ant-btn-primary {
background-color: var(--color-primary);
border-color: var(--color-primary);
color: white;
}
.review-files-page .ant-btn-primary:hover {
background-color: var(--color-primary-hover);
border-color: var(--color-primary-hover);
color: white;
}
/* 确保链接按钮样式一致 */
.review-files-page .ant-btn-primary a {
color: white !important;
text-decoration: none;
}
.review-files-page .ant-btn-primary:hover a {
color: white !important;
}
.review-files-page .ant-btn-default a {
color: rgba(0, 0, 0, 0.85);
text-decoration: none;
}
.review-files-page .ant-btn-default:hover a {
color: var(--color-primary);
}
/* 结果统计样式 */
.review-files-page .result-summary {
display: flex;
align-items: center;
margin-bottom: 16px;
}
.review-files-page .result-tag {
display: inline-flex;
align-items: center;
margin-right: 24px;
font-size: 14px;
}
.review-files-page .result-tag-count {
font-weight: 400;
margin-left: 4px;
}
/* 确保使用Table组件后的样式一致性 */
/* .review-files-page .files-table th {
text-align: left !important;
}
.review-files-page .files-table td {
text-align: center !important;
vertical-align: middle !important;
} */
-240
View File
@@ -1,240 +0,0 @@
/* 评查文件列表页面样式 */
.review-files-page {
/* 所有样式都包含在此命名空间内 */
}
/* 筛选区域 */
.review-files-page .card-container {
margin-bottom: 16px;
}
/* 搜索框样式 */
.review-files-page .search-box {
display: flex;
align-items: center;
width: 100%;
}
.review-files-page .search-box .form-input {
flex: 1;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
padding: 8px 12px;
border: none;
outline: none;
}
.review-files-page .search-box .ant-btn {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
/* 表格样式 */
.review-files-page .ant-table {
width: 100%;
border-collapse: collapse;
}
.review-files-page .ant-table th {
background-color: #fafafa;
font-weight: 500;
padding: 16px;
text-align: left;
border-bottom: 1px solid #f0f0f0;
}
.review-files-page .ant-table td {
padding: 16px;
border-bottom: 1px solid #f0f0f0;
}
.review-files-page .ant-table tr:hover {
background-color: rgba(0, 0, 0, 0.02);
}
/* 文件类型徽章 */
.review-files-page .file-type-badge {
display: inline-flex;
align-items: center;
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
}
.review-files-page .file-type-badge i {
margin-right: 4px;
font-size: 14px;
}
.review-files-page .file-type-contract {
background-color: #e6f7ff;
color: #1890ff;
}
.review-files-page .file-type-license {
background-color: #f6ffed;
color: #52c41a;
}
.review-files-page .file-type-punishment {
background-color: #fff7e6;
color: #fa8c16;
}
.review-files-page .file-type-report {
background-color: #e6fffb;
color: #13c2c2;
}
.review-files-page .file-type-other {
background-color: #f9f0ff;
color: #722ed1;
}
/* 状态徽章 */
.review-files-page .status-badge {
display: inline-flex;
align-items: center;
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
}
.review-files-page .status-pass {
background-color: #f6ffed;
color: #52c41a;
}
.review-files-page .status-warning {
background-color: #fffbe6;
color: #faad14;
}
.review-files-page .status-fail {
background-color: #fff1f0;
color: #f5222d;
}
.review-files-page .status-pending {
background-color: #f9f0ff;
color: #722ed1;
}
/* 严重程度指示器 */
.review-files-page .severity-indicator {
display: inline-block;
width: 16px;
height: 16px;
border-radius: 50%;
margin-right: 8px;
vertical-align: middle;
}
.review-files-page .severity-info {
background-color: #1890ff;
}
.review-files-page .severity-warning {
background-color: #faad14;
}
.review-files-page .severity-error {
background-color: #f5222d;
}
.review-files-page .severity-critical {
background-color: #722ed1;
}
/* 分页样式 */
.review-files-page .pagination {
display: flex;
justify-content: flex-end;
margin-top: 16px;
padding: 16px;
}
.review-files-page .pagination-item {
min-width: 32px;
height: 32px;
margin-right: 8px;
display: inline-flex;
justify-content: center;
align-items: center;
border: 1px solid #d9d9d9;
border-radius: 4px;
font-size: 14px;
cursor: pointer;
transition: all 0.3s;
background-color: transparent;
}
.review-files-page .pagination-item:hover {
border-color: var(--color-primary, #1677ff);
color: var(--color-primary, #1677ff);
}
.review-files-page .pagination-item.active {
border-color: var(--color-primary, #1677ff);
background-color: var(--color-primary, #1677ff);
color: white;
}
.review-files-page .pagination-item.disabled {
color: rgba(0, 0, 0, 0.25);
border-color: #d9d9d9;
cursor: not-allowed;
}
/* 表单组件样式 */
.review-files-page .form-select {
width: 100%;
height: 38px;
padding: 8px 12px;
border: 1px solid #d9d9d9;
border-radius: 4px;
font-size: 14px;
transition: all 0.2s;
background-color: white;
}
.review-files-page .form-select:focus {
border-color: var(--color-primary, #1677ff);
box-shadow: 0 0 0 2px rgba(22, 119, 255, 0.2);
outline: none;
}
/* 内容卡片样式 */
.review-files-page .content-card .ant-card-body {
padding: 0 !important;
}
/* 文本颜色辅助类 */
.review-files-page .text-success {
color: #52c41a;
}
.review-files-page .text-warning {
color: #faad14;
}
.review-files-page .text-error {
color: #f5222d;
}
.review-files-page .text-secondary {
color: rgba(0, 0, 0, 0.45);
}
/* 错误容器样式 */
.review-files-page .error-container {
text-align: center;
padding: 48px;
background-color: #fff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
max-width: 500px;
margin: 48px auto;
}
+82 -31
View File
@@ -3,12 +3,21 @@
/* 所有样式都包含在此命名空间内 */
}
/* 新增评查点按钮样式 */
.rules-page .btn-add-rule {
@apply bg-[#00684a] text-white;
}
.rules-page .btn-add-rule:hover {
@apply bg-[#005a3f] text-white;
}
/* 筛选区域 */
.rules-page .filter-card {
margin-bottom: 1rem;
}
/* 搜索框样式 */
/* 搜索框样式 - 与search-box.css重复,已注释
.rules-page .search-box {
display: flex;
align-items: center;
@@ -25,13 +34,12 @@
border-bottom-left-radius: 0;
}
/* 无按钮搜索框样式 */
.rules-page .search-box.form-input-only .form-input {
border-radius: 0.25rem;
width: 100%;
}
} */
/* 表格样式 */
/* 表格样式 - 与table.css重复,已注释
.rules-page .ant-table {
width: 100%;
border-collapse: collapse;
@@ -39,7 +47,7 @@
.rules-page .ant-table th {
background-color: #f9f9f9;
font-weight: 400; /* 减少文字粗细度 */
font-weight: 400;
padding: 12px 16px;
text-align: left;
border-bottom: 1px solid #e9ecef;
@@ -48,11 +56,54 @@
.rules-page .ant-table td {
padding: 12px 16px;
border-bottom: 1px solid #e9ecef;
font-weight: 400; /* 减少文字粗细度 */
font-weight: 400;
}
.rules-page .ant-table tr:hover {
background-color: rgba(0, 0, 0, 0.02);
} */
/* 表格自定义样式 */
.rules-page .ant-table {
width: 100%;
border-collapse: collapse;
}
.rules-page .ant-table th {
background-color: #f9f9f9;
font-weight: 500;
padding: 10px;
text-align: center;
border-bottom: 1px solid #e9ecef;
color: #333;
font-size: 14px;
}
.rules-page .ant-table td {
padding: 10px;
border-bottom: 1px solid #e9ecef;
font-weight: 400;
text-align: center;
vertical-align: middle;
font-size: 14px;
}
.rules-page .ant-table tr:hover {
background-color: rgba(0, 0, 0, 0.02);
}
/* 使用Table组件时的样式 */
.rules-page .rules-table th {
text-align: center !important;
}
.rules-page .rules-table td {
text-align: center !important;
}
.rules-page .ant-table .status-column {
text-align: center;
width: 80px;
}
/* 表格操作列样式 */
@@ -63,12 +114,12 @@
/* 操作按钮样式 - 改为文本按钮样式 */
.rules-page .operations-cell .ant-btn {
background: transparent;
border: 1px solid #e9ecef;
border: none;
padding: 4px 8px;
margin-right: 4px;
border-radius: 4px;
font-size: 14px;
color: #495057;
color: #00684a;
height: auto;
min-width: auto;
box-shadow: none;
@@ -80,10 +131,9 @@
display: inline-flex;
align-items: center;
justify-content: center;
border: 1px solid #e4e4e4;
background-color: #ffffff;
color: #333;
border-radius: 4px;
background-color: transparent;
color: #00684a;
border: none;
line-height: 1;
padding: 4px 8px;
font-size: 13px;
@@ -97,23 +147,24 @@
}
.rules-page .operation-btn:hover {
border-color: #00684a;
color: #00684a;
color: #005a3f;
text-decoration: underline;
}
.rules-page .operation-btn-danger {
background-color: #f5222d;
border-color: #f5222d;
color: white;
color: #f5222d;
background-color: transparent;
border: none;
}
.rules-page .operation-btn-danger:hover {
background-color: #cf1f29;
border-color: #cf1f29;
color: white !important;
color: #cf1f29 !important;
background-color: transparent;
border: none;
text-decoration: underline;
}
/* 状态点样式 */
/* 状态点样式 - 与status-dot.css重复,已注释
.rules-page .status-dot {
display: inline-block;
width: 8px;
@@ -128,9 +179,9 @@
.rules-page .status-dot-default {
background-color: #d9d9d9;
}
} */
/* 标签自定义样式 */
/* 标签自定义样式 - 与tag.css重复,已注释
.rules-page .ant-tag {
display: inline-flex;
align-items: center;
@@ -138,7 +189,7 @@
font-size: 12px;
border-radius: 4px;
margin-right: 8px;
font-weight: 400; /* 减少文字粗细度 */
font-weight: 400;
}
.rules-page .ant-tag-blue {
@@ -169,9 +220,9 @@
.rules-page .ant-tag-red {
background-color: rgba(245, 34, 45, 0.1);
color: #f5222d;
}
} */
/* 分页样式 */
/* 分页样式 - 与pagination.css重复,已注释
.rules-page .ant-pagination {
display: flex;
align-items: center;
@@ -228,7 +279,7 @@
border-radius: 4px;
height: 32px;
padding: 0 8px;
}
} */
/* 卡片内容调整 */
.rules-page .content-card .ant-card-body {
@@ -304,6 +355,7 @@
margin-bottom: 16px;
}
/* 卡片样式 - 与card.css重复,已注释
.rules-page .ant-card {
background-color: white;
border-radius: 8px;
@@ -314,16 +366,15 @@
.rules-page .ant-card-body {
padding: 16px;
}
} */
/* 按钮样式修正 */
/* 按钮样式修正 - 与button.css重复,已注释
.rules-page .ant-btn-sm {
height: 32px;
padding: 0 8px;
font-size: 14px;
}
/* 按钮悬停覆盖样式 */
.rules-page .ant-btn-primary {
color: white !important;
}
@@ -331,7 +382,7 @@
.rules-page .ant-btn-primary:hover {
color: white !important;
background-color: #005a3f;
}
} */
/* 针对操作列中的按钮
.rules-page .operations-cell .ant-btn-default:hover {
+5
View File
@@ -21,4 +21,9 @@ export default defineConfig({
}),
tsconfigPaths(),
],
server: {
host: '0.0.0.0',
port: 5173,
open: true
},
});