Merge branch 'shiy' into awen
# Conflicts: # app/root.tsx
This commit is contained in:
@@ -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'
|
||||
},
|
||||
{
|
||||
@@ -97,6 +97,12 @@ export function Sidebar({ onToggle, collapsed }: SidebarProps) {
|
||||
path: '/settings',
|
||||
icon: 'ri-settings-4-line',
|
||||
children: [
|
||||
{
|
||||
id: 'config-lists',
|
||||
title: '配置列表',
|
||||
path: '/config-lists',
|
||||
icon: 'ri-list-check-3'
|
||||
},
|
||||
{
|
||||
id: 'basic-settings',
|
||||
title: '基础设置',
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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 };
|
||||
@@ -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,19 +54,24 @@ 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>
|
||||
<select
|
||||
className="form-select ant-pagination-options-size-changer"
|
||||
value={pageSize}
|
||||
onChange={(e) => onPageSizeChange(Number(e.target.value))}
|
||||
>
|
||||
{pageSizeOptions.map(size => (
|
||||
<option key={size} value={size}>{size} 条/页</option>
|
||||
))}
|
||||
</select>
|
||||
<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>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -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}`}>
|
||||
<input
|
||||
<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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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'
|
||||
@@ -23,11 +27,14 @@ export function StatusDot({
|
||||
statusType === 'warning' ? '警告' :
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
+5
-5
@@ -14,7 +14,7 @@ import { Layout } from "~/components/layout/Layout";
|
||||
import { ErrorBoundary as AppErrorBoundary } from "~/components/error/ErrorBoundary";
|
||||
import "remixicon/fonts/remixicon.css";
|
||||
// 导入样式
|
||||
import mainStyles from "~/styles/main.css?url";
|
||||
import styles from "~/styles/main.css?url";
|
||||
|
||||
// 添加客户端hydration错误处理
|
||||
// if (typeof window !== "undefined") {
|
||||
@@ -42,10 +42,10 @@ export const meta: MetaFunction = () => {
|
||||
// 使用links函数为应用加载CSS和其他资源
|
||||
export function links() {
|
||||
return [
|
||||
{ rel: "stylesheet", href: mainStyles },
|
||||
{ rel: "preconnect", href: "https://fonts.googleapis.com" },
|
||||
{ rel: "preconnect", href: "https://fonts.gstatic.com", crossOrigin: "anonymous" },
|
||||
{ rel: "stylesheet", href: "https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;500;700&display=swap" }
|
||||
{ rel: "stylesheet", href: styles },
|
||||
// { rel: "preconnect", href: "https://fonts.googleapis.com" },
|
||||
// { rel: "preconnect", href: "https://fonts.gstatic.com", crossOrigin: "anonymous" },
|
||||
// { rel: "stylesheet", href: "https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;500;700&display=swap" }
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.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>
|
||||
);
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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>
|
||||
|
||||
|
||||
@@ -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: "评查点列表"
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
/**
|
||||
/* *
|
||||
* 按钮组件样式
|
||||
*/
|
||||
|
||||
/* 基础按钮 */
|
||||
/* 基础按钮
|
||||
.btn {
|
||||
@apply inline-flex items-center justify-center px-4 py-2
|
||||
border border-transparent rounded-md font-medium text-sm
|
||||
focus:outline-none focus:ring-2 focus:ring-offset-2 transition-all duration-200
|
||||
disabled:opacity-50 disabled:cursor-not-allowed;
|
||||
}
|
||||
} */
|
||||
|
||||
/* 按钮尺寸 */
|
||||
/* 按钮尺寸
|
||||
.btn-xs {
|
||||
@apply px-2.5 py-1 text-xs;
|
||||
}
|
||||
@@ -21,13 +21,17 @@
|
||||
|
||||
.btn-lg {
|
||||
@apply px-5 py-2.5 text-base;
|
||||
}
|
||||
} */
|
||||
|
||||
/* 按钮类型 */
|
||||
/* 按钮类型
|
||||
.btn-primary {
|
||||
@apply bg-[#00684a] text-white hover:bg-[#005a3f] focus:ring-[#00684a];
|
||||
}
|
||||
|
||||
.btn-default {
|
||||
@apply bg-white text-gray-800 hover:bg-gray-300 focus:ring-gray-300;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
@apply bg-gray-200 text-gray-800 hover:bg-gray-300 focus:ring-gray-300;
|
||||
}
|
||||
@@ -44,9 +48,9 @@
|
||||
.btn-text {
|
||||
@apply bg-transparent text-[#00684a] shadow-none border-none
|
||||
hover:bg-[rgba(0,104,74,0.05)] focus:ring-0;
|
||||
}
|
||||
} */
|
||||
|
||||
/* 图标按钮 */
|
||||
/* 图标按钮
|
||||
.btn-icon {
|
||||
@apply p-2 rounded-full;
|
||||
}
|
||||
@@ -57,9 +61,9 @@
|
||||
|
||||
.btn-icon i, .btn-icon svg {
|
||||
@apply text-current w-5 h-5;
|
||||
}
|
||||
} */
|
||||
|
||||
/* 按钮组 */
|
||||
/* 按钮组
|
||||
.btn-group {
|
||||
@apply inline-flex;
|
||||
}
|
||||
@@ -74,4 +78,29 @@
|
||||
|
||||
.btn-group .btn:last-child {
|
||||
@apply rounded-r-md;
|
||||
}
|
||||
} */
|
||||
|
||||
|
||||
/* 按钮样式 */
|
||||
.ant-btn {
|
||||
@apply inline-flex items-center justify-center px-4 py-2
|
||||
border border-transparent rounded-md font-medium text-sm
|
||||
focus:outline-none focus:ring-2 focus:ring-offset-2 transition-all duration-200
|
||||
disabled:opacity-50 disabled:cursor-not-allowed;
|
||||
}
|
||||
|
||||
.ant-btn-primary {
|
||||
@apply bg-[#00684a] text-white hover:bg-[#005a3f] focus:ring-[#00684a];
|
||||
}
|
||||
|
||||
.ant-btn-default {
|
||||
@apply bg-white border border-gray-300 text-gray-800 hover:border-[#00684a] focus:ring-[#00684a];
|
||||
}
|
||||
|
||||
.ant-btn-danger {
|
||||
@apply bg-[#f5222d] text-white hover:bg-[#cf1f29] focus:ring-[#f5222d];
|
||||
}
|
||||
|
||||
.ant-btn-sm {
|
||||
@apply px-3 py-1.5 text-sm;
|
||||
}
|
||||
@@ -26,7 +26,7 @@
|
||||
|
||||
/* 卡片内容 */
|
||||
.card-body {
|
||||
@apply p-5;
|
||||
@apply p-1;
|
||||
}
|
||||
|
||||
/* 卡片底部 */
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -32,7 +32,7 @@
|
||||
}
|
||||
|
||||
.form-select {
|
||||
@apply pr-10;
|
||||
@apply pr-10 border-gray-300 border-[1px];
|
||||
}
|
||||
|
||||
.form-textarea {
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
-24
@@ -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;
|
||||
@@ -197,30 +214,6 @@
|
||||
|
||||
/* === UI组件 === */
|
||||
|
||||
/* 按钮样式 */
|
||||
.ant-btn {
|
||||
@apply inline-flex items-center justify-center px-4 py-2
|
||||
border border-transparent rounded-md font-medium text-sm
|
||||
focus:outline-none focus:ring-2 focus:ring-offset-2 transition-all duration-200
|
||||
disabled:opacity-50 disabled:cursor-not-allowed;
|
||||
}
|
||||
|
||||
.ant-btn-primary {
|
||||
@apply bg-[#00684a] text-white hover:bg-[#005a3f] focus:ring-[#00684a];
|
||||
}
|
||||
|
||||
.ant-btn-default {
|
||||
@apply bg-gray-200 text-gray-800 hover:bg-gray-300 focus:ring-gray-300;
|
||||
}
|
||||
|
||||
.ant-btn-danger {
|
||||
@apply bg-[#f5222d] text-white hover:bg-[#cf1f29] focus:ring-[#f5222d];
|
||||
}
|
||||
|
||||
.ant-btn-sm {
|
||||
@apply px-3 py-1.5 text-sm;
|
||||
}
|
||||
|
||||
/* 卡片组件 */
|
||||
.card {
|
||||
@apply bg-white rounded-lg shadow-sm border border-gray-200 p-4;
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* 配置列表页样式
|
||||
*/
|
||||
|
||||
/* 配置页面容器 */
|
||||
.config-lists {
|
||||
@apply p-6;
|
||||
}
|
||||
|
||||
/* 表格区域 */
|
||||
.config-lists .config-table {
|
||||
@apply w-full;
|
||||
}
|
||||
|
||||
/* 选择框focus状态 */
|
||||
.config-lists .form-select:focus {
|
||||
border-color: #00684a;
|
||||
box-shadow: 0 0 0 2px rgba(0, 104, 74, 0.2);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* 环境标签 */
|
||||
.config-lists .env-tag {
|
||||
@apply inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium;
|
||||
}
|
||||
|
||||
.config-lists .env-tag-dev {
|
||||
@apply bg-blue-100 text-blue-900;
|
||||
}
|
||||
|
||||
.config-lists .env-tag-test {
|
||||
@apply bg-yellow-100 text-yellow-900;
|
||||
}
|
||||
|
||||
.config-lists .env-tag-prod {
|
||||
@apply bg-green-100 text-green-900;
|
||||
}
|
||||
|
||||
/* 操作列样式 */
|
||||
.config-lists .operations-cell {
|
||||
@apply flex space-x-2;
|
||||
}
|
||||
|
||||
.config-lists .operation-btn {
|
||||
@apply text-sm flex items-center text-[--color-primary] bg-transparent hover:underline p-2;
|
||||
}
|
||||
|
||||
|
||||
.config-lists .operation-btn i {
|
||||
@apply mr-1;
|
||||
}
|
||||
|
||||
/* 详情模态框样式 */
|
||||
.config-lists .config-detail-content {
|
||||
@apply space-y-4;
|
||||
}
|
||||
|
||||
.config-lists .config-detail-item {
|
||||
@apply mb-4;
|
||||
}
|
||||
|
||||
.config-lists .config-detail-label {
|
||||
@apply text-sm text-gray-500 mb-1;
|
||||
}
|
||||
|
||||
.config-lists .config-detail-value {
|
||||
@apply text-base;
|
||||
}
|
||||
|
||||
.config-lists .config-detail-code {
|
||||
@apply bg-gray-50 p-3 rounded text-sm overflow-x-auto;
|
||||
}
|
||||
|
||||
.config-lists .ant-btn-primary{
|
||||
@apply !text-white
|
||||
}
|
||||
|
||||
/* 响应式调整 */
|
||||
@screen md {
|
||||
.config-lists .config-filter-form {
|
||||
@apply grid-cols-2;
|
||||
}
|
||||
}
|
||||
|
||||
@screen lg {
|
||||
.config-lists .config-filter-form {
|
||||
@apply grid-cols-4;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
/**
|
||||
* 配置新增/编辑页样式
|
||||
*/
|
||||
|
||||
/* 页面容器 */
|
||||
.config-new-page {
|
||||
@apply p-6;
|
||||
}
|
||||
|
||||
/* 表单卡片 */
|
||||
.config-new-page .config-form-card {
|
||||
@apply p-6;
|
||||
}
|
||||
|
||||
/* 表单样式 */
|
||||
.config-new-page .config-form {
|
||||
@apply space-y-6;
|
||||
}
|
||||
|
||||
.config-new-page .form-row {
|
||||
@apply grid grid-cols-1 md:grid-cols-2 gap-6;
|
||||
}
|
||||
|
||||
.config-new-page .form-row-full {
|
||||
@apply md:col-span-2;
|
||||
}
|
||||
|
||||
.config-new-page .form-group {
|
||||
@apply mb-4;
|
||||
}
|
||||
|
||||
.config-new-page .form-label {
|
||||
@apply block text-sm font-medium mb-1 text-gray-700;
|
||||
}
|
||||
|
||||
.config-new-page .form-label.required::after {
|
||||
content: " *";
|
||||
@apply text-red-500;
|
||||
}
|
||||
|
||||
.config-new-page .form-input,
|
||||
.config-new-page .form-select,
|
||||
.config-new-page .form-textarea {
|
||||
@apply block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-primary-500 focus:border-primary-500 sm:text-sm;
|
||||
}
|
||||
|
||||
.config-new-page .form-textarea {
|
||||
@apply font-mono;
|
||||
}
|
||||
|
||||
.config-new-page .form-checkbox {
|
||||
@apply h-4 w-4 text-primary border-gray-300 rounded focus:ring-primary-500;
|
||||
}
|
||||
|
||||
.config-new-page .form-checkbox-label {
|
||||
@apply ml-2 block text-sm text-gray-900;
|
||||
}
|
||||
|
||||
.config-new-page .input-error {
|
||||
@apply border-red-500;
|
||||
}
|
||||
|
||||
.config-new-page .error-message {
|
||||
@apply text-sm text-red-500 mt-1;
|
||||
}
|
||||
|
||||
.config-new-page .general-error {
|
||||
@apply bg-red-50 p-3 rounded text-center;
|
||||
}
|
||||
|
||||
.config-new-page .form-help {
|
||||
@apply text-xs text-gray-500 mt-1;
|
||||
}
|
||||
|
||||
.config-new-page .form-actions {
|
||||
@apply flex justify-end space-x-3;
|
||||
}
|
||||
|
||||
.config-new-page .inline {
|
||||
@apply inline-block;
|
||||
}
|
||||
|
||||
/* 标签按钮样式 */
|
||||
.config-new-page .tag-buttons {
|
||||
@apply flex flex-wrap gap-2;
|
||||
}
|
||||
|
||||
.config-new-page .tag-button {
|
||||
@apply inline-flex items-center px-3 py-1 rounded-full text-xs font-medium
|
||||
bg-gray-100 text-gray-700 border border-gray-200 cursor-pointer transition-all duration-200
|
||||
hover:bg-[rgba(0,104,74,0.1)] hover:border-[#00684a] hover:text-[#00684a];
|
||||
}
|
||||
|
||||
.config-new-page .tag-button.active {
|
||||
@apply bg-[rgba(0,104,74,0.15)] border-[#00684a] text-[#00684a];
|
||||
}
|
||||
|
||||
/* JSON编辑器 */
|
||||
.config-new-page .json-editor {
|
||||
@apply w-full min-h-[320px] font-mono text-sm leading-relaxed
|
||||
border border-gray-300 rounded-md p-3 bg-gray-50 text-gray-800
|
||||
focus:outline-none focus:ring-primary-500 focus:border-primary-500;
|
||||
}
|
||||
|
||||
.config-new-page .editor-actions {
|
||||
@apply text-right mt-2;
|
||||
}
|
||||
|
||||
/* 示例卡片 */
|
||||
.config-new-page .example-card {
|
||||
@apply h-full flex flex-col bg-gray-50 rounded-md border border-gray-200 overflow-hidden;
|
||||
}
|
||||
|
||||
.config-new-page .example-header {
|
||||
@apply p-3 border-b border-gray-200 bg-gray-100;
|
||||
}
|
||||
|
||||
.config-new-page .example-title {
|
||||
@apply font-medium text-gray-700;
|
||||
}
|
||||
|
||||
.config-new-page .example-content {
|
||||
@apply p-3 flex-grow overflow-auto;
|
||||
}
|
||||
|
||||
.config-new-page .example-pre {
|
||||
@apply m-0 font-mono text-sm leading-relaxed text-gray-800;
|
||||
}
|
||||
|
||||
.config-new-page .example-footer {
|
||||
@apply p-3 border-t border-gray-200 bg-gray-100;
|
||||
}
|
||||
|
||||
/* 代码语法高亮基础样式 */
|
||||
.config-new-page .code-json .key { @apply text-blue-600; }
|
||||
.config-new-page .code-json .string { @apply text-green-600; }
|
||||
.config-new-page .code-json .number { @apply text-purple-600; }
|
||||
.config-new-page .code-json .boolean { @apply text-blue-600; }
|
||||
.config-new-page .code-json .null { @apply text-gray-500; }
|
||||
@@ -98,19 +98,23 @@
|
||||
|
||||
/* 状态标签 */
|
||||
.status-badge {
|
||||
@apply inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium;
|
||||
@apply inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium;
|
||||
}
|
||||
|
||||
.status-badge.status-success {
|
||||
@apply bg-[rgba(0,104,74,0.1)] text-[#00684a];
|
||||
@apply bg-green-100 text-green-900;
|
||||
}
|
||||
|
||||
.status-badge.status-warning {
|
||||
@apply bg-[rgba(250,173,20,0.1)] text-[#faad14];
|
||||
@apply bg-yellow-100 text-yellow-900;
|
||||
}
|
||||
|
||||
.status-badge.status-error {
|
||||
@apply bg-[rgba(245,34,45,0.1)] text-[#f5222d];
|
||||
@apply bg-red-100 text-red-900;
|
||||
}
|
||||
|
||||
.status-badge.status-processing {
|
||||
@apply bg-blue-100 text-blue-900;
|
||||
}
|
||||
|
||||
/* 卡片样式 */
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
} */
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
Binary file not shown.
+5
-3
@@ -4,10 +4,12 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>中国烟草AI合同及卷宗审核系统 - 新增/编辑系统配置</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/remixicon@2.5.0/fonts/remixicon.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
|
||||
<link href="https://unpkg.com/remixicon@2.5.0/fonts/remixicon.css" rel="stylesheet">
|
||||
<link href="https://unpkg.com/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
|
||||
<!-- <link href="https://cdn.jsdelivr.net/npm/remixicon@2.5.0/fonts/remixicon.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet"> -->
|
||||
<!-- 引入外部CSS文件 -->
|
||||
<link href="../css/main.css" rel="stylesheet">
|
||||
<link href="main.css" rel="stylesheet">
|
||||
<style>
|
||||
.json-editor {
|
||||
width: 100%;
|
||||
|
||||
@@ -21,4 +21,9 @@ export default defineConfig({
|
||||
}),
|
||||
tsconfigPaths(),
|
||||
],
|
||||
server: {
|
||||
host: '0.0.0.0',
|
||||
port: 5173,
|
||||
open: true
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user