diff --git a/app/components/layout/Layout.tsx b/app/components/layout/Layout.tsx index b14d871..78df87f 100644 --- a/app/components/layout/Layout.tsx +++ b/app/components/layout/Layout.tsx @@ -34,7 +34,7 @@ export function Layout({ children }: LayoutProps) {
{/*
*/}
- + {children}
diff --git a/app/components/layout/Sidebar.tsx b/app/components/layout/Sidebar.tsx index 2bd6ed3..a6429c0 100644 --- a/app/components/layout/Sidebar.tsx +++ b/app/components/layout/Sidebar.tsx @@ -58,17 +58,23 @@ export function Sidebar({ onToggle, collapsed }: SidebarProps) { icon: 'ri-folder-open-line' }, { - id: 'rule-list', + id: 'rules-list', title: '评查点列表', path: '/rules', icon: 'ri-list-check-3' }, - // { - // id: 'rule-new', - // title: '新增评查点', - // path: '/rules/new', - // icon: 'ri-add-circle-line' - // } + { + id: 'rules-file', + title: '评查文件列表', + path: '/rules/files', + icon: 'ri-list-check-2' + }, + { + id: 'rule-new', + title: '新增评查点', + path: '/rules/new', + icon: 'ri-add-circle-line' + } ] }, { @@ -125,9 +131,12 @@ export function Sidebar({ onToggle, collapsed }: SidebarProps) { }, []); const toggleMenu = (id: string, e: React.MouseEvent) => { + // 防止事件冒泡和默认行为 e.preventDefault(); e.stopPropagation(); + // console.log('%c父菜单展开/折叠 ===> ', 'background: #f5222d; color: white; padding: 2px 4px; border-radius: 2px;', id); + setExpandedMenus(prev => ({ ...prev, [id]: !prev[id] @@ -140,11 +149,19 @@ export function Sidebar({ onToggle, collapsed }: SidebarProps) { // 处理侧边栏切换事件 const handleToggleSidebar = (e: React.MouseEvent) => { + // console.log('%c侧边栏折叠/展开 ===> ', 'background: #1890ff; color: white; padding: 2px 4px; border-radius: 2px;'); e.preventDefault(); e.stopPropagation(); onToggle(); }; + // 处理子菜单项点击事件 + const handleSubMenuClick = (child: MenuItem, e: React.MouseEvent) => { + // 需要阻止冒泡,否则会触发父级菜单的展开/折叠事件 + e.stopPropagation(); + // console.log('%c子菜单点击 ===> ', 'background: #00684a; color: white; padding: 2px 4px; border-radius: 2px;', child.title, '路径:', child.path); + }; + return (
@@ -181,6 +198,10 @@ export function Sidebar({ onToggle, collapsed }: SidebarProps) { { + e.stopPropagation(); + // console.log('%c单级菜单点击 ===> ', 'background: #52c41a; color: white; padding: 2px 4px; border-radius: 2px;', item.title, '路径:', item.path); + }} > {!collapsed && {item.title}} @@ -188,8 +209,11 @@ export function Sidebar({ onToggle, collapsed }: SidebarProps) { ) : ( <>
toggleMenu(item.id, e)} + className={`sidebar-menu-item flex items-center ${collapsed ? 'justify-center' : 'justify-between'} cursor-pointer z-10`} + onClick={(e) => { + // console.log('%c父菜单点击 ===> ', 'background: #722ed1; color: white; padding: 2px 4px; border-radius: 2px;', item.title); + toggleMenu(item.id, e); + }} role="button" tabIndex={0} aria-expanded={expandedMenus[item.id] || false} @@ -212,7 +236,7 @@ export function Sidebar({ onToggle, collapsed }: SidebarProps) { {(expandedMenus[item.id] || collapsed) && (
{item.children.map((child) => ( @@ -220,6 +244,7 @@ export function Sidebar({ onToggle, collapsed }: SidebarProps) { key={child.id} to={child.path} className={`sidebar-menu-item ${isActive(child.path) ? 'active' : ''} flex items-center ${collapsed ? 'justify-center' : ''}`} + onClick={(e) => handleSubMenuClick(child, e)} > {!collapsed && {child.title}} diff --git a/app/components/ui/Modal.tsx b/app/components/ui/Modal.tsx new file mode 100644 index 0000000..5372d12 --- /dev/null +++ b/app/components/ui/Modal.tsx @@ -0,0 +1,72 @@ +// app/components/ui/Modal.tsx +import React, { useEffect } from 'react'; + +interface ModalProps { + isOpen: boolean; + onClose: () => void; + title: React.ReactNode; + children: React.ReactNode; + footer?: React.ReactNode; + width?: number | string; +} + +export function Modal({ isOpen, onClose, title, children, footer, width = 500 }: ModalProps) { + // 点击ESC键关闭模态框 + useEffect(() => { + const handleEsc = (e: KeyboardEvent) => { + if (e.key === 'Escape' && isOpen) { + onClose(); + } + }; + window.addEventListener('keydown', handleEsc); + return () => window.removeEventListener('keydown', handleEsc); + }, [isOpen, onClose]); + + // 禁用背景滚动 + useEffect(() => { + if (isOpen) { + document.body.style.overflow = 'hidden'; + } else { + document.body.style.overflow = ''; + } + return () => { + document.body.style.overflow = ''; + }; + }, [isOpen]); + + if (!isOpen) return null; + + return ( +
{ + if (e.target === e.currentTarget) onClose(); + }} + > +
e.stopPropagation()} + > +
+

{title}

+ +
+
+ {children} +
+ {footer && ( +
+ {footer} +
+ )} +
+
+ ); +} \ No newline at end of file diff --git a/app/components/ui/Pagination.tsx b/app/components/ui/Pagination.tsx new file mode 100644 index 0000000..12ceab3 --- /dev/null +++ b/app/components/ui/Pagination.tsx @@ -0,0 +1,103 @@ +// app/components/ui/Pagination.tsx +import React from 'react'; + +interface PaginationProps { + currentPage: number; + total: number; + pageSize: number; + onChange: (page: number) => void; + onPageSizeChange?: (pageSize: number) => void; + pageSizeOptions?: number[]; +} + +export function Pagination({ + currentPage, + total, + pageSize, + onChange, + onPageSizeChange, + pageSizeOptions = [10, 20, 50] +}: PaginationProps) { + const totalPages = Math.ceil(total / pageSize); + + // 生成页码数组 + const getPageNumbers = () => { + const pages = []; + const maxVisiblePages = 5; + + if (totalPages <= maxVisiblePages) { + // 总页数小于等于最大可见页数,显示所有页码 + for (let i = 1; i <= totalPages; i++) { + pages.push(i); + } + } else if (currentPage <= 3) { + // 当前页靠近开始 + for (let i = 1; i <= maxVisiblePages; i++) { + pages.push(i); + } + } else if (currentPage >= totalPages - 2) { + // 当前页靠近结尾 + for (let i = totalPages - maxVisiblePages + 1; i <= totalPages; i++) { + pages.push(i); + } + } else { + // 当前页在中间 + for (let i = currentPage - 2; i <= currentPage + 2; i++) { + pages.push(i); + } + } + + return pages; + }; + + return ( +
+ {onPageSizeChange && ( +
+ 共 {total} 条 + +
+ )} + +
+ + + {getPageNumbers().map(page => ( + + ))} + + +
+
+ ); +} \ No newline at end of file diff --git a/app/components/ui/SearchBox.tsx b/app/components/ui/SearchBox.tsx new file mode 100644 index 0000000..0176d66 --- /dev/null +++ b/app/components/ui/SearchBox.tsx @@ -0,0 +1,53 @@ +import React from 'react'; +import { Button } from './Button'; + +interface SearchBoxProps { + placeholder?: string; + defaultValue?: string; + onSearch: (value: string) => void; + buttonText?: string; + className?: string; + name?: string; +} + +export function SearchBox({ + placeholder = '请输入搜索内容', + defaultValue = '', + onSearch, + buttonText = '搜索', + className = '', + name = 'keyword' +}: SearchBoxProps) { + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + const formData = new FormData(e.currentTarget); + const value = formData.get(name) as string; + onSearch(value); + }; + + const handleChange = (e: React.ChangeEvent) => { + // 对于没有按钮的输入框,我们希望在输入时就触发搜索 + if (className.includes('form-input-only')) { + onSearch(e.target.value); + } + }; + + return ( +
+ + {buttonText && !className.includes('form-input-only') && ( + + )} +
+ ); +} \ No newline at end of file diff --git a/app/components/ui/StatusDot.tsx b/app/components/ui/StatusDot.tsx new file mode 100644 index 0000000..ea99793 --- /dev/null +++ b/app/components/ui/StatusDot.tsx @@ -0,0 +1,33 @@ +type StatusType = 'success' | 'error' | 'warning' | 'default' | 'processing'; + +interface StatusDotProps { + status: StatusType | boolean; + text?: string; + className?: string; +} + +export function StatusDot({ + status, + text, + className = '' +}: StatusDotProps) { + // 如果status是布尔值,则转换为对应的状态类型 + const statusType = typeof status === 'boolean' + ? (status ? 'success' : 'default') + : status; + + // 如果没有提供文本,则根据状态类型提供默认文本 + const statusText = text ?? ( + statusType === 'success' ? '启用' : + statusType === 'error' ? '禁用' : + statusType === 'warning' ? '警告' : + statusType === 'processing' ? '处理中' : '未知' + ); + + return ( + + + {statusText} + + ); +} \ No newline at end of file diff --git a/app/components/ui/Tag.tsx b/app/components/ui/Tag.tsx index e3eca30..fde0db7 100644 --- a/app/components/ui/Tag.tsx +++ b/app/components/ui/Tag.tsx @@ -3,58 +3,15 @@ import React from 'react'; export type TagColor = 'blue' | 'green' | 'cyan' | 'purple' | 'orange' | 'red' | 'default'; interface TagProps { - children: React.ReactNode; color?: TagColor; - closable?: boolean; - onClose?: (e: React.MouseEvent) => void; + children: React.ReactNode; className?: string; } -export function Tag({ - children, - color = 'default', - closable = false, - onClose, - className = '', -}: TagProps) { - const baseClasses = 'ant-tag'; - - const colorClasses = { - default: '', - blue: 'ant-tag-blue', - green: 'ant-tag-green', - cyan: 'ant-tag-cyan', - purple: 'ant-tag-purple', - orange: 'ant-tag-orange', - red: 'ant-tag-red' - }; - - const classes = [ - baseClasses, - colorClasses[color], - className - ].filter(Boolean).join(' '); - - const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === 'Enter' || e.key === ' ') { - e.preventDefault(); - onClose?.(e as unknown as React.MouseEvent); - } - }; - +export function Tag({ color = 'default', children, className = '' }: TagProps) { return ( - + {children} - {closable && ( - - )} ); } \ No newline at end of file diff --git a/app/models/rule.ts b/app/models/rule.ts index 97a064e..729044c 100644 --- a/app/models/rule.ts +++ b/app/models/rule.ts @@ -22,14 +22,14 @@ export interface Rule { } export interface RuleGroup { - id: string; - name: string; - code: string; - description: string; - isActive: boolean; - orderIndex: number; - createdAt: string; - updatedAt: string; + id: string; // 分组ID + name: string; // 分组名称 + code: string; // 分组编码 + description: string; // 分组描述 + status: 'active' | 'inactive'; // 分组状态 + sortOrder: number; // 排序顺序 + createdAt: string; // 创建时间 + updatedAt: string; // 更新时间 } export const RULE_TYPE_LABELS: Record = { diff --git a/app/root.tsx b/app/root.tsx index 116abc8..0c813a8 100644 --- a/app/root.tsx +++ b/app/root.tsx @@ -13,7 +13,8 @@ import { import { Layout } from "~/components/layout/Layout"; import { ErrorBoundary as AppErrorBoundary } from "~/components/error/ErrorBoundary"; import "remixicon/fonts/remixicon.css"; -// 不使用import导入样式 +// 导入样式 +import styles from "~/styles/main.css?url"; // 添加客户端hydration错误处理 // if (typeof window !== "undefined") { @@ -38,9 +39,10 @@ export const meta: MetaFunction = () => { ]; }; +// 使用links函数为应用加载CSS和其他资源 export function links() { return [ - { rel: "stylesheet", href: "/tailwind.css" }, // 使用构建后的Tailwind CSS + { 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" } diff --git a/app/routes/_index.tsx b/app/routes/_index.tsx index ce07bd0..358d6ce 100644 --- a/app/routes/_index.tsx +++ b/app/routes/_index.tsx @@ -5,7 +5,7 @@ import { Card } from "~/components/ui/Card"; import { Button } from "~/components/ui/Button"; export const links = () => [ - { rel: "stylesheet", href: "/app/styles/index.css" } + { rel: "stylesheet", href: "app/styles/pages/home.css" } ]; export const meta: MetaFunction = () => { diff --git a/app/routes/files/new.tsx b/app/routes/files/new.tsx new file mode 100644 index 0000000..5c396e6 --- /dev/null +++ b/app/routes/files/new.tsx @@ -0,0 +1,34 @@ +import { MetaFunction } from '@remix-run/node'; +import { Card } from '~/components/ui/Card'; +import { Button } from '~/components/ui/Button'; + +export const meta: MetaFunction = () => { + return [ + { title: "文件上传 - 中国烟草AI合同及卷宗审核系统" }, + { name: "description", content: "上传文件进行智能评查" } + ]; +}; + +export default function FilesNew() { + return ( +
+ {/* 页面标识 */} +
+

当前页面: 文件上传 (files/new.tsx)

+

如果你看到这个提示,说明你正在浏览文件上传页面,路由已成功加载。

+ +
+ + +
+ +

拖拽文件到此处或点击上传

+ +

支持 PDF、DOC、DOCX、XLS、XLSX 等格式

+
+
+
+ ); +} \ No newline at end of file diff --git a/app/routes/rule-groups.$groupId.tsx b/app/routes/rule-groups.$groupId.tsx new file mode 100644 index 0000000..5bd0c0a --- /dev/null +++ b/app/routes/rule-groups.$groupId.tsx @@ -0,0 +1,264 @@ +import { json, redirect, type ActionFunctionArgs, type LoaderFunctionArgs, type MetaFunction } from "@remix-run/node"; +import { Form, useLoaderData, useNavigation, useActionData } from "@remix-run/react"; +import { useState, useEffect } from "react"; + +export const meta: MetaFunction = ({ data }) => { + return [ + { title: `编辑评查点分组 - ${data?.group?.name || '加载中'} - 中国烟草AI合同及卷宗审核系统` }, + { name: "description", content: "编辑评查点分组信息,包括名称、编码、描述和状态" }, + ]; +}; + +// 模拟数据 +const MOCK_GROUPS = [ + { + id: "1", + name: "合同条款类", + code: "CONTRACT", + description: "关于合同条款的评查点分组", + status: "active", + sortOrder: 1, + createdAt: "2024-01-01T00:00:00Z", + updatedAt: "2024-01-01T00:00:00Z", + }, + { + id: "2", + name: "合规性类", + code: "COMPLIANCE", + description: "关于合规性的评查点分组", + status: "active", + sortOrder: 2, + createdAt: "2024-01-01T00:00:00Z", + updatedAt: "2024-01-01T00:00:00Z", + }, + { + id: "3", + name: "风险提示类", + code: "RISK", + description: "关于风险提示的评查点分组", + status: "inactive", + sortOrder: 3, + createdAt: "2024-01-01T00:00:00Z", + updatedAt: "2024-01-01T00:00:00Z", + }, +]; + +export async function loader({ params }: LoaderFunctionArgs) { + const { groupId } = params; + + // 真实环境中,这里会调用API获取数据 + // const response = await fetch(`${process.env.API_URL}/api/rule-groups/${groupId}`); + // if (response.status === 404) { + // throw new Response("评查点分组不存在", { status: 404 }); + // } + // if (!response.ok) { + // throw new Response("获取评查点分组失败", { status: response.status }); + // } + // const group = await response.json(); + + // 使用模拟数据 + const group = MOCK_GROUPS.find(g => g.id === groupId); + if (!group) { + throw new Response("评查点分组不存在", { status: 404 }); + } + + return json({ group }); +} + +export async function action({ request, params }: ActionFunctionArgs) { + const formData = await request.formData(); + const groupId = params.groupId; + + const name = formData.get("name"); + const code = formData.get("code"); + const description = formData.get("description"); + const status = formData.get("status"); + const sortOrder = formData.get("sortOrder"); + + // 基本验证 + const errors = {}; + if (!name) errors.name = "分组名称不能为空"; + if (!code) errors.code = "分组编码不能为空"; + if (Object.keys(errors).length > 0) { + return json({ errors, values: Object.fromEntries(formData) }); + } + + // 构建更新数据 + const updateData = { + name, + code, + description, + status, + sortOrder: Number(sortOrder) || 0, + }; + + // 真实环境中,这里会调用API更新数据 + // const response = await fetch(`${process.env.API_URL}/api/rule-groups/${groupId}`, { + // method: "PUT", + // headers: { "Content-Type": "application/json" }, + // body: JSON.stringify(updateData), + // }); + // + // if (!response.ok) { + // throw new Response("更新评查点分组失败", { status: response.status }); + // } + + // 模拟更新成功 + console.log('保存分组数据:', { id: groupId, ...updateData }); + + // 重定向回列表页 + return redirect('/rule-groups'); +} + +export default function EditRuleGroup() { + const { group } = useLoaderData(); + const actionData = useActionData(); + const navigation = useNavigation(); + + const isSubmitting = navigation.state === "submitting"; + + const [formData, setFormData] = useState({ + name: group.name, + code: group.code, + description: group.description || "", + status: group.status, + sortOrder: group.sortOrder.toString(), + }); + + // 当actionData中有错误时,保留用户输入的值 + useEffect(() => { + if (actionData?.values) { + setFormData({ + name: actionData.values.name, + code: actionData.values.code, + description: actionData.values.description || "", + status: actionData.values.status, + sortOrder: actionData.values.sortOrder, + }); + } + }, [actionData]); + + const handleChange = (e) => { + const { name, value } = e.target; + setFormData(prev => ({ ...prev, [name]: value })); + }; + + return ( +
+
+

编辑评查点分组

+
+ +
+
+
+
+ + + {actionData?.errors?.name && ( +

{actionData.errors.name}

+ )} +
+ +
+ + + {actionData?.errors?.code && ( +

{actionData.errors.code}

+ )} +
+
+ +
+ + +
+ +
+
+ + +
+ +
+ + +
+
+ +
+ { + e.preventDefault(); + window.history.back(); + }} + > + 取消 + + +
+
+
+
+ ); +} \ No newline at end of file diff --git a/app/routes/rule-groups._index.tsx b/app/routes/rule-groups._index.tsx index 5c98772..608d5ad 100644 --- a/app/routes/rule-groups._index.tsx +++ b/app/routes/rule-groups._index.tsx @@ -1,384 +1,336 @@ -import React from 'react'; -import { Link } from "@remix-run/react"; import { json, type MetaFunction } from "@remix-run/node"; -import { useLoaderData, useNavigate } from "@remix-run/react"; +import { useLoaderData, Link, useNavigate } 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 stylesUrl from "~/styles/pages/rule-groups.css"; -// 引入CSS -// export function links() { -// return [ -// { rel: "stylesheet", href: stylesUrl } -// ]; -// } -export const meta: MetaFunction = () => { - return [ - { title: "中国烟草AI合同及卷宗审核系统 - 评查点分组列表" }, - { name: "description", content: "评查点分组管理" } - ]; -}; - - -// 分组接口定义 +// 定义数据类型 interface RuleGroup { id: string; name: string; code: string; ruleCount: number; - childGroupCount: number; - isActive: boolean; - parentId: string | null; + subGroupCount: number; + status: 'active' | 'inactive'; createdAt: string; - level: 1 | 2; // 1-一级分组,2-二级分组 + children?: RuleGroup[]; } -interface LoaderData { - groups: RuleGroup[]; +export const handle = { + breadcrumb: "评查点分组" +}; + +export const meta: MetaFunction = () => { + return [ + { title: "评查点分组 - 中国烟草AI合同及卷宗审核系统" }, + { name: "description", content: "管理评查点分组,包括创建、编辑和删除分组" }, + ]; +}; + +export function links() { + return [{ rel: "stylesheet", href: indexStyles }]; } +// 模拟数据 +const MOCK_GROUPS: RuleGroup[] = [ + { + id: "1", + name: "合同基本要素检查", + code: "contract-base", + ruleCount: 18, + subGroupCount: 12, + status: "active", + createdAt: "2023-10-01 14:30", + children: [ + { + id: "2", + name: "必备要素检查", + code: "essential-elements", + ruleCount: 7, + subGroupCount: 0, + status: "active", + createdAt: "2023-10-02 10:15", + }, + { + id: "3", + name: "合同主体检查", + code: "contract-parties", + ruleCount: 5, + subGroupCount: 0, + status: "active", + createdAt: "2023-10-03 16:20", + } + ] + }, + { + id: "4", + name: "销售合同专项检查", + code: "contract-sales", + ruleCount: 12, + subGroupCount: 5, + status: "active", + createdAt: "2023-10-05 09:30", + children: [ + { + id: "6", + name: "付款条件检查", + code: "payment-terms", + ruleCount: 5, + subGroupCount: 0, + status: "active", + createdAt: "2023-10-05 14:45", + } + ] + }, + { + id: "5", + name: "行政处罚规范性检查", + code: "punishment", + ruleCount: 8, + subGroupCount: 0, + status: "inactive", + createdAt: "2023-10-08 11:45", + } +]; + export async function loader() { - // 模拟数据,实际项目中应该从API获取 - const groups: RuleGroup[] = [ - { - id: "1", - name: "合同基本要素检查", - code: "contract-base", - ruleCount: 18, - childGroupCount: 2, - isActive: true, - parentId: null, - createdAt: "2023-10-01 14:30", - level: 1 - }, - { - id: "2", - name: "必备要素检查", - code: "essential-elements", - ruleCount: 7, - childGroupCount: 0, - isActive: true, - parentId: "1", - createdAt: "2023-10-02 10:15", - level: 2 - }, - { - id: "3", - name: "合同主体检查", - code: "contract-parties", - ruleCount: 5, - childGroupCount: 0, - isActive: true, - parentId: "1", - createdAt: "2023-10-02 11:40", - level: 2 - }, - { - id: "4", - name: "销售合同专项检查", - code: "sales-contract", - ruleCount: 10, - childGroupCount: 2, - isActive: true, - parentId: null, - createdAt: "2023-10-03 09:20", - level: 1 - }, - { - id: "5", - name: "交付条款检查", - code: "delivery-terms", - ruleCount: 4, - childGroupCount: 0, - isActive: true, - parentId: "4", - createdAt: "2023-10-03 14:30", - level: 2 - }, - { - id: "6", - name: "付款条款检查", - code: "payment-terms", - ruleCount: 6, - childGroupCount: 0, - isActive: true, - parentId: "4", - createdAt: "2023-10-03 15:45", - level: 2 - }, - { - id: "7", - name: "采购合同专项检查", - code: "purchase-contract", - ruleCount: 8, - childGroupCount: 0, - isActive: true, - parentId: null, - createdAt: "2023-10-04 10:15", - level: 1 - }, - { - id: "8", - name: "行政处罚规范性检查", - code: "admin-punishment", - ruleCount: 12, - childGroupCount: 0, - isActive: false, - parentId: null, - createdAt: "2023-10-05 16:30", - level: 1 - } - ]; - - return json({ groups }); + return json({ groups: MOCK_GROUPS }); } -export default function RuleGroupsPage() { +export default function RuleGroupsIndex() { const { groups } = useLoaderData(); const navigate = useNavigate(); + const [searchText, setSearchText] = useState(""); + const [groupCode, setGroupCode] = useState(""); + const [expandedGroups, setExpandedGroups] = useState([]); - // 过滤出父级分组和子级分组 - const parentGroups = groups.filter(group => group.parentId === null); - - // 根据父级ID获取子分组 - const getChildGroups = (parentId: string) => { - return groups.filter(group => group.parentId === parentId); + // 处理展开/收起 + const toggleGroup = (groupId: string) => { + setExpandedGroups(prev => + prev.includes(groupId) + ? prev.filter(id => id !== groupId) + : [...prev, groupId] + ); }; - // 模拟删除操作 - const handleDelete = (id: string) => { - if (window.confirm("确定要删除该分组吗?删除后无法恢复,且会删除该分组下的所有评查点。")) { - alert(`删除分组: ${id}`); - } + // 展开/收起全部 + const toggleAll = (expand: boolean) => { + setExpandedGroups(expand ? groups.map(g => g.id) : []); }; - // 创建新分组 - const handleCreate = () => { - navigate("/rule-groups/new"); - }; - - // 展开/收起状态(实际项目中可以使用useState管理) - const toggleExpand = (groupId: string) => { - const childRows = document.querySelectorAll(`.child-of-${groupId}`); - childRows.forEach(row => { - (row as HTMLElement).style.display = - (row as HTMLElement).style.display === "none" ? "table-row" : "none"; - }); - - // 切换图标 - const icon = document.querySelector(`span.expand-icon[data-group-id="${groupId}"] i`); - if (icon) { - icon.classList.toggle("ri-arrow-down-s-line"); - icon.classList.toggle("ri-arrow-right-s-line"); + // 处理删除分组 + const handleDeleteGroup = (groupId: string) => { + if (confirm("确定要删除该分组吗?此操作将同时删除该分组下的所有评查点,且不可恢复。")) { + console.log('删除分组ID:', groupId); + // 实际应用中,这里会调用API删除数据 } }; - // 键盘处理器 - const handleKeyDown = (e: React.KeyboardEvent, groupId: string) => { - if (e.key === 'Enter' || e.key === ' ') { - e.preventDefault(); - toggleExpand(groupId); - } + // 处理搜索名称 + const handleNameSearch = (value: string) => { + setSearchText(value); + // 实际项目中这里可能需要调用API或过滤本地数据 }; - + + // 处理搜索编码 + const handleCodeSearch = (value: string) => { + setGroupCode(value); + // 实际项目中这里可能需要调用API或过滤本地数据 + }; + + // 处理表格数据,包括父子级关系 + const processedData = groups.flatMap(group => { + // 先添加父级分组 + const result: (RuleGroup & { isParent?: boolean, parentId?: string })[] = [ + { ...group, isParent: true } + ]; + + // 如果有子级分组并且当前已展开,则添加子级分组 + if (group.children && expandedGroups.includes(group.id)) { + group.children.forEach(child => { + result.push({ ...child, parentId: group.id }); + }); + } + + return result; + }); + return ( -
- {/* 页面标识 */} -
-

当前页面: 评查点分组列表 (rule-groups._index.tsx)

-

如果你看到这个提示,说明你已成功到达评查点分组列表页面。

- -
- +
{/* 页面头部 */}

评查点分组管理

- - -
{/* 搜索栏 */} -
-
-
-
- - -
-
- - -
-
- - -
-
- - -
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ +
-
+ {/* 数据表格 */} -
-
-
- - - - - - - - - - - - - {parentGroups.map(parent => ( - - {/* 一级分组 */} - - - - - - - + + + + + + + ))} + +
分组名称分组编码评查点数量状态创建时间操作
-
- toggleExpand(parent.id)} - onKeyDown={(e) => handleKeyDown(e, parent.id)} - role="button" - tabIndex={0} - aria-label="展开/收起" - > - - - - {parent.name} - - 一级分组 -
-
{parent.code} - - {parent.ruleCount} - - {parent.childGroupCount > 0 && ( - - | 子分组: {parent.childGroupCount} - - )} - - - {parent.isActive ? '启用' : '禁用'} - {parent.createdAt} - {item.code} + + {item.ruleCount} + + {item.subGroupCount > 0 && ( + + | 子分组: {item.subGroupCount} + + )} + + + {item.createdAt} + + +
+
+ + {/* 分页 */} +
+
+ 共 {groups.length} 条记录,每页显示 10 条 +
+
+ + +
-
+
); } \ No newline at end of file diff --git a/app/routes/rule-groups.new.tsx b/app/routes/rule-groups.new.tsx new file mode 100644 index 0000000..4aa3a90 --- /dev/null +++ b/app/routes/rule-groups.new.tsx @@ -0,0 +1,207 @@ +// app/routes/rule-groups.new.tsx +import { json, redirect, type ActionFunctionArgs, type MetaFunction } from "@remix-run/node"; +import { Form, useNavigation, useActionData } from "@remix-run/react"; +import { useState, useEffect } from "react"; + +export const meta: MetaFunction = () => { + return [ + { title: "新建评查点分组 - 中国烟草AI合同及卷宗审核系统" }, + { name: "description", content: "创建新的评查点分组,包括分组名称、编码、描述和状态" }, + ]; +}; + +export async function action({ request }: ActionFunctionArgs) { + const formData = await request.formData(); + + const name = formData.get("name"); + const code = formData.get("code"); + const description = formData.get("description"); + const status = formData.get("status") || "active"; + const sortOrder = formData.get("sortOrder") || "0"; + + // 基本验证 + const errors = {}; + if (!name) errors.name = "分组名称不能为空"; + if (!code) errors.code = "分组编码不能为空"; + if (Object.keys(errors).length > 0) { + return json({ errors, values: Object.fromEntries(formData) }); + } + + // 构建创建数据 + const createData = { + name, + code, + description, + status, + sortOrder: Number(sortOrder) || 0, + }; + + // 真实环境中,这里会调用API创建数据 + // const response = await fetch(`${process.env.API_URL}/api/rule-groups`, { + // method: "POST", + // headers: { "Content-Type": "application/json" }, + // body: JSON.stringify(createData), + // }); + // + // if (!response.ok) { + // throw new Response("创建评查点分组失败", { status: response.status }); + // } + + // 模拟创建成功 + console.log('创建分组数据:', createData); + + // 重定向回列表页 + return redirect('/rule-groups'); +} + +export default function NewRuleGroup() { + const actionData = useActionData(); + const navigation = useNavigation(); + + const isSubmitting = navigation.state === "submitting"; + + const [formData, setFormData] = useState({ + name: "", + code: "", + description: "", + status: "active", + sortOrder: "0", + }); + + // 当actionData中有错误时,保留用户输入的值 + useEffect(() => { + if (actionData?.values) { + setFormData({ + name: actionData.values.name, + code: actionData.values.code, + description: actionData.values.description || "", + status: actionData.values.status, + sortOrder: actionData.values.sortOrder, + }); + } + }, [actionData]); + + const handleChange = (e) => { + const { name, value } = e.target; + setFormData(prev => ({ ...prev, [name]: value })); + }; + + return ( +
+
+

新建评查点分组

+
+ +
+
+
+
+ + + {actionData?.errors?.name && ( +

{actionData.errors.name}

+ )} +
+ +
+ + + {actionData?.errors?.code && ( +

{actionData.errors.code}

+ )} +
+
+ +
+ + +
+ +
+
+ + +
+ +
+ + +
+
+ +
+ { + e.preventDefault(); + window.history.back(); + }} + > + 取消 + + +
+
+
+
+ ); +} \ No newline at end of file diff --git a/app/routes/rule-groups.tsx b/app/routes/rule-groups.tsx index 1aae504..a33a0d2 100644 --- a/app/routes/rule-groups.tsx +++ b/app/routes/rule-groups.tsx @@ -1,20 +1,10 @@ +// app/routes/rule-groups.tsx import { Outlet } from "@remix-run/react"; -import { type MetaFunction } from "@remix-run/node"; -export const links = () => [ - { rel: "stylesheet", href: "/rule-groups.css" } -]; - -export const meta: MetaFunction = () => { - return [ - { title: "中国烟草AI合同及卷宗审核系统 - 评查点分组管理" }, - { name: "description", content: "评查点分组管理页面" } - ]; +export const handle = { + breadcrumb: "评查规则库" }; -/** - * 评查点分组管理路由布局 - */ -export default function RuleGroupsLayout() { - return ; -} \ No newline at end of file +export default function RuleGroupsLayout() { + return +} \ No newline at end of file diff --git a/app/routes/rules._index.tsx b/app/routes/rules._index.tsx index 65c3bc7..71f5b08 100644 --- a/app/routes/rules._index.tsx +++ b/app/routes/rules._index.tsx @@ -1,20 +1,30 @@ -import React from 'react'; -import { json, type MetaFunction, type LoaderFunctionArgs } from "@remix-run/node"; -import { useLoaderData, useSearchParams } from "@remix-run/react"; +import React, { useState } from 'react'; +import { json, type MetaFunction, type LoaderFunctionArgs, redirect } from "@remix-run/node"; +import { useLoaderData, useSearchParams, useSubmit } from "@remix-run/react"; import { Button } from '~/components/ui/Button'; import { Card } from '~/components/ui/Card'; -import { Tag, type TagColor } from '~/components/ui/Tag'; +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"; 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'; export const links = () => [ - { rel: "stylesheet", href: "/rules_index.css" } + { rel: "stylesheet", href: rulesStyles } ]; +export const handle = { + breadcrumb: "评查点列表" +}; + export const meta: MetaFunction = () => { return [ { title: "中国烟草AI合同及卷宗审核系统 - 评查点列表" }, - { name: "description", content: "评查点管理列表" } + { name: "description", content: "管理评查点规则,支持根据类型、规则组和状态进行筛选" }, + { name: "keywords", content: "评查点,合同审核,规则管理,中国烟草" } ]; }; @@ -25,6 +35,9 @@ interface LoaderData { name: string; }[]; totalCount: number; + currentPage: number; + pageSize: number; + totalPages: number; } export async function loader({ request }: LoaderFunctionArgs) { @@ -33,128 +46,269 @@ export async function loader({ request }: LoaderFunctionArgs) { const groupId = url.searchParams.get("groupId") || ""; const isActive = url.searchParams.get("isActive") || ""; const keyword = url.searchParams.get("keyword") || ""; + const currentPage = parseInt(url.searchParams.get("page") || "1", 10); + const pageSize = parseInt(url.searchParams.get("pageSize") || "10", 10); - // 模拟数据,实际项目中应从API获取 - const rules: Rule[] = [ - { - id: "1", - code: "CP001", - name: "合同主体信息完整性检查", - ruleGroupId: "1", - groupName: "合同基本要素检查", - ruleType: "essential", - priority: "high", - description: "检查合同中是否完整包含签约方的基本信息,包括名称、地址、法定代表人等", - checkMethod: "automatic", - prompt: "检查合同主体双方信息是否完整,包括企业名称、注册地址、法定代表人或授权代表、联系方式等", - isActive: true, - createdAt: "2023-06-15 10:30", - updatedAt: "2023-06-15 10:30" - }, - { - id: "2", - code: "CP002", - name: "合同金额一致性校验", - ruleGroupId: "1", - groupName: "合同基本要素检查", - ruleType: "content", - priority: "high", - description: "检查合同大小写金额是否一致", - checkMethod: "automatic", - prompt: "检查合同中的金额大写和小写表示是否一致,如¥10,000.00(壹万元整)", - isActive: true, - createdAt: "2023-06-20 14:15", - updatedAt: "2023-06-20 14:15" - }, - { - id: "3", - code: "CP003", - name: "保密条款合规性审核", - ruleGroupId: "1", - groupName: "合同基本要素检查", - ruleType: "legal", - priority: "medium", - description: "检查合同是否包含保密条款并符合行业要求", - checkMethod: "mixed", - prompt: "检查合同中的保密条款是否完整、清晰,包含保密范围、期限、违约责任等", - isActive: true, - createdAt: "2023-07-05 09:45", - updatedAt: "2023-07-05 09:45" - }, - { - id: "4", - code: "CP004", - name: "合同签约日期格式检查", - ruleGroupId: "1", - groupName: "合同基本要素检查", - ruleType: "format", - priority: "low", - description: "检查合同签约日期格式是否规范", - checkMethod: "automatic", - prompt: "检查合同签约日期格式是否符合YYYY年MM月DD日的规范格式", - isActive: false, - createdAt: "2023-07-10 16:20", - updatedAt: "2023-07-10 16:20" - }, - { - id: "5", - code: "CP005", - name: "违约责任条款完整性检查", - ruleGroupId: "2", - groupName: "销售合同专项检查", - ruleType: "legal", - priority: "high", - description: "检查合同违约责任条款是否明确、完整", - checkMethod: "mixed", - prompt: "检查合同中的违约责任条款是否包含违约情形、违约金计算方式、责任承担方式等内容", - isActive: true, - createdAt: "2023-07-15 11:30", - updatedAt: "2023-07-15 11:30" + try { + // 模拟数据,实际项目中应从API获取 + const rules: Rule[] = [ + { + id: "1", + code: "CP001", + name: "合同主体信息完整性检查", + ruleGroupId: "1", + groupName: "合同基本要素检查", + ruleType: "essential", + priority: "high", + description: "检查合同中是否完整包含签约方的基本信息,包括名称、地址、法定代表人等", + checkMethod: "automatic", + prompt: "检查合同主体双方信息是否完整,包括企业名称、注册地址、法定代表人或授权代表、联系方式等", + isActive: true, + createdAt: "2023-06-15 10:30", + updatedAt: "2023-06-15 10:30" + }, + { + id: "2", + code: "CP002", + name: "合同金额一致性校验", + ruleGroupId: "1", + groupName: "合同基本要素检查", + ruleType: "content", + priority: "high", + description: "检查合同大小写金额是否一致", + checkMethod: "automatic", + prompt: "检查合同中的金额大写和小写表示是否一致,如¥10,000.00(壹万元整)", + isActive: true, + createdAt: "2023-06-20 14:15", + updatedAt: "2023-06-20 14:15" + }, + { + id: "3", + code: "CP003", + name: "保密条款合规性审核", + ruleGroupId: "1", + groupName: "合同基本要素检查", + ruleType: "legal", + priority: "medium", + description: "检查合同是否包含保密条款并符合行业要求", + checkMethod: "mixed", + prompt: "检查合同中的保密条款是否完整、清晰,包含保密范围、期限、违约责任等", + isActive: true, + createdAt: "2023-07-05 09:45", + updatedAt: "2023-07-05 09:45" + }, + { + id: "4", + code: "CP004", + name: "合同签约日期格式检查", + ruleGroupId: "1", + groupName: "合同基本要素检查", + ruleType: "format", + priority: "low", + description: "检查合同签约日期格式是否规范", + checkMethod: "automatic", + prompt: "检查合同签约日期格式是否符合YYYY年MM月DD日的规范格式", + isActive: false, + createdAt: "2023-07-10 16:20", + updatedAt: "2023-07-10 16:20" + }, + { + id: "5", + code: "CP005", + name: "违约责任条款完整性检查", + ruleGroupId: "2", + groupName: "销售合同专项检查", + ruleType: "legal", + priority: "high", + description: "检查合同违约责任条款是否明确、完整", + checkMethod: "mixed", + prompt: "检查合同中的违约责任条款是否包含违约情形、违约金计算方式、责任承担方式等内容", + isActive: true, + createdAt: "2023-07-15 11:30", + updatedAt: "2023-07-15 11:30" + }, + { + id: "6", + code: "CP006", + name: "交货期限有效性检查", + ruleGroupId: "2", + groupName: "销售合同专项检查", + ruleType: "business", + priority: "medium", + description: "检查合同中交货期限是否明确、合理", + checkMethod: "automatic", + prompt: "检查合同中是否明确约定了交货期限,并且期限设置是否合理", + isActive: true, + createdAt: "2023-08-01 14:40", + updatedAt: "2023-08-01 14:40" + }, + { + id: "7", + code: "CP007", + name: "合同条款矛盾性检查", + ruleGroupId: "3", + groupName: "采购合同专项检查", + ruleType: "legal", + priority: "high", + description: "检查合同条款之间是否存在矛盾或冲突", + checkMethod: "mixed", + prompt: "分析合同各条款,检查是否存在相互矛盾或冲突的内容", + isActive: true, + createdAt: "2023-08-10 09:15", + updatedAt: "2023-08-10 09:15" + } + ]; + + const groups = [ + { id: "1", name: "合同基本要素检查" }, + { id: "2", name: "销售合同专项检查" }, + { id: "3", name: "采购合同专项检查" }, + { id: "4", name: "专卖许可证审核规则" }, + { id: "5", name: "行政处罚规范性检查" } + ]; + + // 过滤数据 + let filteredRules = [...rules]; + + if (ruleType) { + filteredRules = filteredRules.filter(rule => rule.ruleType === ruleType); } - ]; - - const groups = [ - { id: "1", name: "合同基本要素检查" }, - { id: "2", name: "销售合同专项检查" }, - { id: "3", name: "采购合同专项检查" }, - { id: "4", name: "专卖许可证审核规则" }, - { id: "5", name: "行政处罚规范性检查" } - ]; - - // 过滤数据 - let filteredRules = [...rules]; - - if (ruleType) { - filteredRules = filteredRules.filter(rule => rule.ruleType === ruleType); + + if (groupId) { + filteredRules = filteredRules.filter(rule => rule.ruleGroupId === groupId); + } + + if (isActive) { + const activeValue = isActive === 'true'; + filteredRules = filteredRules.filter(rule => rule.isActive === activeValue); + } + + if (keyword) { + const lowerKeyword = keyword.toLowerCase(); + filteredRules = filteredRules.filter(rule => + rule.name.toLowerCase().includes(lowerKeyword) || + rule.code.toLowerCase().includes(lowerKeyword) + ); + } + + // 计算分页信息 + const totalCount = filteredRules.length; + const totalPages = Math.ceil(totalCount / pageSize); + + // 验证页码范围 + if (currentPage < 1 || (totalCount > 0 && currentPage > totalPages)) { + // 如果页码超出范围,重定向到第一页 + const newUrl = new URL(request.url); + newUrl.searchParams.set('page', '1'); + return redirect(newUrl.pathname + newUrl.search); + } + + // 分页截取 + const startIndex = (currentPage - 1) * pageSize; + const endIndex = startIndex + pageSize; + const paginatedRules = filteredRules.slice(startIndex, endIndex); + + return json({ + rules: paginatedRules, + groups, + totalCount, + currentPage, + pageSize, + totalPages + }, { + headers: { + // 添加缓存控制,在生产环境中可以调整 + "Cache-Control": "max-age=60, s-maxage=180" + } + }); + } catch (error) { + console.error('加载评查点列表失败:', error); + throw new Response('加载评查点列表失败', { status: 500 }); } - - if (groupId) { - filteredRules = filteredRules.filter(rule => rule.ruleGroupId === groupId); - } - - if (isActive) { - const activeValue = isActive === 'true'; - filteredRules = filteredRules.filter(rule => rule.isActive === activeValue); - } - - if (keyword) { - const lowerKeyword = keyword.toLowerCase(); - filteredRules = filteredRules.filter(rule => - rule.name.toLowerCase().includes(lowerKeyword) || - rule.code.toLowerCase().includes(lowerKeyword) - ); - } - - return json({ - rules: filteredRules, - groups, - totalCount: rules.length - }); } +export async function action({ request }: LoaderFunctionArgs) { + const formData = await request.formData(); + const _action = formData.get('_action'); + const ruleId = formData.get('ruleId'); + + if (!ruleId) { + return json({ success: false, error: "缺少评查点ID" }, { status: 400 }); + } + + try { + if (_action === 'delete') { + // 实际项目中应调用API删除评查点 + console.log(`删除评查点 ${ruleId}`); + + // 模拟API调用 + // const response = await fetch(`/api/rules/${ruleId}`, { + // method: 'DELETE', + // }); + + // if (!response.ok) { + // throw new Error(`删除失败: ${response.status}`); + // } + + return json({ success: true }); + } + + if (_action === 'duplicate') { + // 实际项目中应调用API复制评查点 + console.log(`复制评查点 ${ruleId}`); + + // 模拟API调用 + // const response = await fetch(`/api/rules/${ruleId}/duplicate`, { + // method: 'POST', + // }); + + // if (!response.ok) { + // throw new Error(`复制失败: ${response.status}`); + // } + + return json({ success: true }); + } + + return json({ success: false, error: "未知操作" }, { status: 400 }); + } catch (error) { + console.error('操作评查点失败:', error); + return json({ success: false, error: "操作失败" }, { status: 500 }); + } +} + +export function ErrorBoundary() { + return ( +
+

出错了

+

加载评查点列表时发生错误。请稍后再试,或联系管理员。

+ +
+ ); +} + +// 规则类型和优先级的描述标签映射 +const typeLabels = { + 'essential': '基本要素类', + 'content': '内容合规类', + 'legal': '法律风险类', + 'format': '格式规范类', + 'business': '业务专项类' +}; + +const priorityLabels = { + 'high': '高', + 'medium': '中', + 'low': '低' +}; + export default function RulesList() { - const { rules, groups } = useLoaderData(); + const { rules, groups, totalCount, currentPage, pageSize, totalPages } = useLoaderData(); const [searchParams, setSearchParams] = useSearchParams(); + const submit = useSubmit(); + + // 状态管理 + const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); + const [ruleToDelete, setRuleToDelete] = useState(null); const handleFilterChange = (e: React.ChangeEvent) => { const { name, value } = e.target; @@ -166,14 +320,13 @@ export default function RulesList() { newParams.delete(name); } + // 切换筛选条件时,重置到第一页 + newParams.set('page', '1'); + setSearchParams(newParams); }; - const handleSearch = (e: React.FormEvent) => { - e.preventDefault(); - const formData = new FormData(e.target as HTMLFormElement); - const keyword = formData.get('keyword') as string; - + const handleSearch = (keyword: string) => { const newParams = new URLSearchParams(searchParams); if (keyword) { newParams.set('keyword', keyword); @@ -181,33 +334,53 @@ export default function RulesList() { newParams.delete('keyword'); } + // 搜索时,重置到第一页 + newParams.set('page', '1'); + setSearchParams(newParams); }; - const handleCopy = (rule: Rule) => { - // 实际项目中应调用API复制规则 - alert(`复制规则: ${rule.name}`); + const handleDeleteClick = (rule: Rule) => { + setRuleToDelete(rule); + setShowDeleteConfirm(true); }; - const handleDelete = (rule: Rule) => { - // 实际项目中应调用API删除规则 - if (window.confirm(`确定要删除评查点"${rule.name}"吗?`)) { - alert(`删除规则: ${rule.name}`); - } + const confirmDelete = () => { + if (!ruleToDelete) return; + + const formData = new FormData(); + formData.append('_action', 'delete'); + formData.append('ruleId', ruleToDelete.id); + + submit(formData, { method: 'post' }); + setShowDeleteConfirm(false); + setRuleToDelete(null); + }; + + const handleCopy = (rule: Rule) => { + const formData = new FormData(); + formData.append('_action', 'duplicate'); + formData.append('ruleId', rule.id); + + submit(formData, { method: 'post' }); + }; + + const handlePageChange = (page: number) => { + const newParams = new URLSearchParams(searchParams); + newParams.set('page', page.toString()); + setSearchParams(newParams); + }; + + const handlePageSizeChange = (e: React.ChangeEvent) => { + const newPageSize = e.target.value; + const newParams = new URLSearchParams(searchParams); + newParams.set('pageSize', newPageSize); + newParams.set('page', '1'); // 更改每页条数时,重置到第一页 + setSearchParams(newParams); }; return ( -
- {/* 页面标识 */} -
-

当前页面: 评查点列表 (rules._index.tsx)

-

如果你看到这个提示,说明你已成功到达评查点列表页面。

- -
- +
{/* 页面头部 */}

评查点管理

@@ -217,28 +390,30 @@ export default function RulesList() {
{/* 筛选区域 */} - -
+ +
- +
- +
- -
- - -
+ +
{/* 评查点列表 */} - +
- - - - - - - - - - - +
评查点编码评查点名称评查点类型所属规则组优先级状态创建时间操作
+ + + + + + + + + + - {rules.map((rule) => { - const typeColor = RULE_TYPE_COLORS[rule.ruleType] as TagColor; - const priorityColor = RULE_PRIORITY_COLORS[rule.priority] as TagColor; - - return ( - - - - - - - - - - - ); - })} + {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 ( + + + + + + + + + + + ); + }) + ) : ( + + + + )}
评查点编码评查点名称评查点类型所属规则组优先级状态创建时间操作
{rule.code}{rule.name} - {RULE_TYPE_LABELS[rule.ruleType]} - {rule.groupName} - {RULE_PRIORITY_LABELS[rule.priority]} - - {rule.isActive ? ( - - - 启用 - - ) : ( - - - 禁用 - - )} - {rule.createdAt} - - - -
{rule.code}{rule.name} + + {typeLabels[rule.ruleType as keyof typeof typeLabels] || RULE_TYPE_LABELS[rule.ruleType]} + + {rule.groupName} + + {priorityLabels[rule.priority as keyof typeof priorityLabels] || RULE_PRIORITY_LABELS[rule.priority]} + + + + {rule.createdAt} + + 编辑 + + + +
+ 暂无评查点数据 +
+ + {/* 分页 */} + {totalCount > 0 && ( +
+
+ 共 {totalCount} 条 + +
+ +
+ + + {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 ( + + ); + })} + + +
+
+ )}
+ + {/* 删除确认对话框 */} + {showDeleteConfirm && ruleToDelete && ( +
+
+

确认删除

+

确定要删除评查点“{ruleToDelete.name}”吗?

+
+ + +
+
+
+ )}
); } \ No newline at end of file diff --git a/app/routes/rules.files.tsx b/app/routes/rules.files.tsx new file mode 100644 index 0000000..dd827dd --- /dev/null +++ b/app/routes/rules.files.tsx @@ -0,0 +1,586 @@ +import { json, type MetaFunction, type LoaderFunctionArgs } from "@remix-run/node"; +import { useLoaderData, useSearchParams, useSubmit } from "@remix-run/react"; +import { Button } from "~/components/ui/Button"; +import { Card } from "~/components/ui/Card"; +import { Table } from "~/components/ui/Table"; +import { SearchBox } from "~/components/ui/SearchBox"; +import rulesFilesStyles from "~/styles/pages/rules_files.css?url"; + +export const links = () => [ + { rel: "stylesheet", href: rulesFilesStyles } +]; + +export const handle = { + breadcrumb: "评查文件列表" +}; + +export const meta: MetaFunction = () => { + return [ + { title: "评查文件列表 - 中国烟草AI合同及卷宗审核系统" }, + { name: "description", content: "管理系统中所有上传的评查文件,支持按文件类型、评查状态进行筛选" }, + { name: "keywords", content: "评查文件,合同审核,中国烟草,文件管理" } + ]; +}; + +// 评查文件类型枚举 +export enum FileType { + CONTRACT = 'contract', + LICENSE = 'license', + PUNISHMENT = 'punishment', + REPORT = 'report', + OTHER = 'other' +} + +// 评查状态枚举 +export enum ReviewStatus { + PASS = 'pass', + WARNING = 'warning', + FAIL = 'fail', + PENDING = 'pending' +} + +// 日期范围枚举 +export enum DateRange { + ALL = 'all', + TODAY = 'today', + WEEK = 'week', + MONTH = 'month', + CUSTOM = 'custom' +} + +// 文件类型标签映射 +export const FILE_TYPE_LABELS: Record = { + [FileType.CONTRACT]: '合同文档', + [FileType.LICENSE]: '专卖许可证', + [FileType.PUNISHMENT]: '行政处罚', + [FileType.REPORT]: '报表文档', + [FileType.OTHER]: '其他文档' +}; + +// 评查状态标签映射 +export const REVIEW_STATUS_LABELS: Record = { + [ReviewStatus.PASS]: '通过', + [ReviewStatus.WARNING]: '警告', + [ReviewStatus.FAIL]: '不通过', + [ReviewStatus.PENDING]: '待人工确认' +}; + +// 评查文件模型 +interface ReviewFile { + id: string; + fileName: string; + fileCode: string; // 文件编号 + fileType: FileType; + fileSize: number; + uploadTime: string; + reviewStatus: ReviewStatus; + issueCount: number; + issues: Array<{ + severity: 'info' | 'warning' | 'error' | 'critical'; + message: string; + }>; + createdBy: string; +} + +interface LoaderData { + files: ReviewFile[]; + totalCount: number; + currentPage: number; + pageSize: number; + totalPages: number; +} + +export async function loader({ request }: LoaderFunctionArgs) { + const url = new URL(request.url); + const fileType = url.searchParams.get("fileType") || ""; + const reviewStatus = url.searchParams.get("reviewStatus") || ""; + const dateRange = url.searchParams.get("dateRange") || ""; + const keyword = url.searchParams.get("keyword") || ""; + const sortOrder = url.searchParams.get("sortOrder") || "upload_time_desc"; + const currentPage = parseInt(url.searchParams.get("page") || "1", 10); + const pageSize = parseInt(url.searchParams.get("pageSize") || "10", 10); + + try { + // 模拟数据,实际项目中应从API获取 + const mockFiles: ReviewFile[] = [ + { + id: "1", + fileName: "烟草产品销售合同(2023版).pdf", + fileCode: "XS-2023-1025-001", + fileType: FileType.CONTRACT, + fileSize: 1024 * 1024 * 2.5, // 2.5MB + uploadTime: "2023-10-25 14:30:45", + reviewStatus: ReviewStatus.WARNING, + issueCount: 3, + issues: [ + { severity: "warning", message: "付款条件描述不明确" }, + { severity: "warning", message: "违约责任条款缺失" }, + { severity: "warning", message: "签章不完整" } + ], + createdBy: "张三" + }, + { + id: "2", + fileName: "2023年度烟草专卖零售许可证.pdf", + fileCode: "LS-2023-0058", + fileType: FileType.LICENSE, + fileSize: 1024 * 1024 * 1.2, // 1.2MB + uploadTime: "2023-10-24 10:15:22", + reviewStatus: ReviewStatus.PASS, + issueCount: 0, + issues: [], + createdBy: "李四" + }, + { + id: "3", + fileName: "XX公司违规处罚决定书.pdf", + fileCode: "处罚[2023]42号", + fileType: FileType.PUNISHMENT, + fileSize: 1024 * 1024 * 3.1, // 3.1MB + uploadTime: "2023-10-23 16:45:30", + reviewStatus: ReviewStatus.FAIL, + issueCount: 2, + issues: [ + { severity: "error", message: "处罚依据条款引用错误" }, + { severity: "error", message: "处罚金额超出规定范围" } + ], + createdBy: "王五" + }, + { + id: "4", + fileName: "烟草设备采购协议.docx", + fileCode: "CG-2023-0089", + fileType: FileType.CONTRACT, + fileSize: 1024 * 1024 * 0.8, // 0.8MB + uploadTime: "2023-10-22 09:22:15", + reviewStatus: ReviewStatus.PENDING, + issueCount: 1, + issues: [ + { severity: "warning", message: "交付日期条款存在歧义,需人工确认" } + ], + createdBy: "赵六" + }, + { + id: "5", + fileName: "2023年度销售额报表.xlsx", + fileCode: "BB-2023-Q3", + fileType: FileType.REPORT, + fileSize: 1024 * 1024 * 0.5, // 0.5MB + uploadTime: "2023-10-20 14:05:38", + reviewStatus: ReviewStatus.PASS, + issueCount: 0, + issues: [], + createdBy: "钱七" + } + ]; + + // 过滤数据 + let filteredFiles = [...mockFiles]; + + if (fileType) { + filteredFiles = filteredFiles.filter(file => file.fileType === fileType); + } + + if (reviewStatus) { + filteredFiles = filteredFiles.filter(file => file.reviewStatus === reviewStatus); + } + + if (dateRange) { + const now = new Date(); + const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()); + + switch (dateRange) { + case DateRange.TODAY: + filteredFiles = filteredFiles.filter(file => { + const fileDate = new Date(file.uploadTime.split(' ')[0]); + return fileDate >= today; + }); + break; + case DateRange.WEEK: + const weekStart = new Date(today); + weekStart.setDate(today.getDate() - today.getDay()); + filteredFiles = filteredFiles.filter(file => { + const fileDate = new Date(file.uploadTime.split(' ')[0]); + return fileDate >= weekStart; + }); + break; + 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; + } + } + + if (keyword) { + const lowerKeyword = keyword.toLowerCase(); + filteredFiles = filteredFiles.filter(file => + file.fileName.toLowerCase().includes(lowerKeyword) || + file.fileCode.toLowerCase().includes(lowerKeyword) + ); + } + + // 排序 + switch (sortOrder) { + case "upload_time_desc": + filteredFiles.sort((a, b) => new Date(b.uploadTime).getTime() - new Date(a.uploadTime).getTime()); + break; + case "upload_time_asc": + filteredFiles.sort((a, b) => new Date(a.uploadTime).getTime() - new Date(b.uploadTime).getTime()); + break; + case "issue_count_desc": + filteredFiles.sort((a, b) => b.issueCount - a.issueCount); + break; + case "issue_count_asc": + filteredFiles.sort((a, b) => a.issueCount - b.issueCount); + break; + } + + // 计算分页信息 + const totalCount = filteredFiles.length; + const totalPages = Math.ceil(totalCount / pageSize); + + // 分页截取 + const startIndex = (currentPage - 1) * pageSize; + const endIndex = startIndex + pageSize; + const paginatedFiles = filteredFiles.slice(startIndex, endIndex); + + return json({ + files: paginatedFiles, + totalCount, + currentPage, + pageSize, + totalPages + }, { + headers: { + "Cache-Control": "max-age=60, s-maxage=180" + } + }); + } catch (error) { + console.error('加载评查文件列表失败:', error); + throw new Response('加载评查文件列表失败', { status: 500 }); + } +} + +export function ErrorBoundary() { + return ( +
+

出错了

+

加载评查文件列表时发生错误。请稍后再试,或联系管理员。

+ +
+ ); +} + +export default function ReviewFilesList() { + const { files, totalCount, currentPage, pageSize, totalPages } = useLoaderData(); + const [searchParams, setSearchParams] = useSearchParams(); + + const handleFilterChange = (e: React.ChangeEvent) => { + 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 handleSearch = (keyword: string) => { + const newParams = new URLSearchParams(searchParams); + if (keyword) { + newParams.set('keyword', keyword); + } else { + newParams.delete('keyword'); + } + + // 搜索时,重置到第一页 + newParams.set('page', '1'); + + setSearchParams(newParams); + }; + + const handlePageChange = (page: number) => { + const newParams = new URLSearchParams(searchParams); + newParams.set('page', page.toString()); + setSearchParams(newParams); + }; + + // 渲染问题摘要 + const renderIssues = (issues: ReviewFile['issues']) => { + if (issues.length === 0) { + return ( +
+ 所有评查点均通过 +
+ ); + } + + return ( +
+ {issues.slice(0, 3).map((issue, index) => ( +
+ + {issue.message} +
+ ))} +
+ ); + }; + + // 渲染文件图标 + const renderFileIcon = (fileName: string) => { + if (fileName.endsWith('.pdf')) { + return ; + } else if (fileName.endsWith('.docx') || fileName.endsWith('.doc')) { + return ; + } else if (fileName.endsWith('.xlsx') || fileName.endsWith('.xls')) { + return ; + } else { + return ; + } + }; + + return ( +
+ {/* 页面头部 */} +
+
+

评查文件列表

+
+ + 总文件数: + {totalCount} +
+
+ +
+ + {/* 筛选区域 */} + +
+
+
文件类型
+ +
+ +
+
评查状态
+ +
+ +
+
时间范围
+ +
+ +
+
搜索
+
+ +
+
+ +
+ +
+
+
+ + {/* 文件列表 */} + + + + + + + + + + + + + + {files.length > 0 ? ( + files.map((file) => ( + + + + + + + + + )) + ) : ( + + + + )} + +
文件名称文件类型上传时间评查状态问题摘要操作
+
+ {renderFileIcon(file.fileName)} +
+
{file.fileName}
+
+ {file.fileType === FileType.CONTRACT && "合同编号:"} + {file.fileType === FileType.LICENSE && "许可证号:"} + {file.fileType === FileType.PUNISHMENT && "文号:"} + {file.fileType === FileType.REPORT && "报表编号:"} + {file.fileCode} +
+
+
+
+ + {file.fileType === FileType.CONTRACT && } + {file.fileType === FileType.LICENSE && } + {file.fileType === FileType.PUNISHMENT && } + {file.fileType === FileType.REPORT && } + {file.fileType === FileType.OTHER && } + {FILE_TYPE_LABELS[file.fileType]} + + + {file.uploadTime.split(' ')[0]} +
+ {file.uploadTime.split(' ')[1]} +
+ + {file.reviewStatus === ReviewStatus.PASS && } + {file.reviewStatus === ReviewStatus.WARNING && } + {file.reviewStatus === ReviewStatus.FAIL && } + {file.reviewStatus === ReviewStatus.PENDING && } + {REVIEW_STATUS_LABELS[file.reviewStatus]} + {file.issueCount > 0 && ` (${file.issueCount})`} + + + {renderIssues(file.issues)} + + {file.reviewStatus === ReviewStatus.PENDING ? ( + + ) : ( + + )} + +
+ 暂无文件数据 +
+ + {/* 分页 */} + {totalCount > 0 && ( +
+ + + {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 ( + + ); + })} + + +
+ )} +
+
+ ); +} \ No newline at end of file diff --git a/app/routes/rules.tsx b/app/routes/rules.tsx index 482b9a6..90a0010 100644 --- a/app/routes/rules.tsx +++ b/app/routes/rules.tsx @@ -1,20 +1,27 @@ 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", content: "规则管理页面" } + { title: "评查规则管理 - 中国烟草AI合同及卷宗审核系统" }, + { + name: "description", + content: "评查规则管理模块,包括评查点列表、创建和编辑功能" + } ]; }; +export const handle = { + breadcrumb: "评查规则库" +}; + /** * 规则管理路由布局 */ export default function RulesLayout() { - return ( - <> - - - ); + return ; } \ No newline at end of file diff --git a/app/styles/base.css b/app/styles/base.css deleted file mode 100644 index 00c0b05..0000000 --- a/app/styles/base.css +++ /dev/null @@ -1,97 +0,0 @@ -/** - * 基础样式文件 - * 包含全局变量与Tailwind基础配置 - */ - -@tailwind base; -@tailwind components; -@tailwind utilities; - -@layer base { - :root { - /* 主题颜色变量 */ - --color-primary: #00684a; - --color-primary-hover: #005a3f; - --color-primary-light: rgba(0, 104, 74, 0.1); - - /* 成功、警告、错误颜色 */ - --color-success: #52c41a; - --color-warning: #faad14; - --color-error: #f5222d; - - /* 中性色 */ - --color-gray-50: #f8f9fa; - --color-gray-100: #f1f3f5; - --color-gray-200: #e9ecef; - --color-gray-300: #dee2e6; - --color-gray-400: #ced4da; - --color-gray-500: #adb5bd; - --color-gray-600: #868e96; - --color-gray-700: #495057; - --color-gray-800: #343a40; - --color-gray-900: #212529; - - /* 字体设置 */ - --font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; - - /* 间距倍数基准 */ - --spacing-base: 0.25rem; - - /* 圆角 */ - --radius-sm: 0.125rem; - --radius-md: 0.25rem; - --radius-lg: 0.5rem; - --radius-xl: 1rem; - - /* 阴影 */ - --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05); - --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); - --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); - - /* 过渡 */ - --transition-normal: 0.2s ease; - } - - /* 基本元素样式 */ - html, body { - @apply font-sans text-gray-800 antialiased; - scroll-behavior: smooth; - } - - /* 文字选择颜色 */ - ::selection { - @apply bg-[rgba(0,104,74,0.2)] text-[#00684a]; - } - - /* 滚动条美化 */ - ::-webkit-scrollbar { - @apply w-2 h-2; - } - - ::-webkit-scrollbar-track { - @apply bg-gray-100; - } - - ::-webkit-scrollbar-thumb { - @apply bg-gray-300 rounded-full hover:bg-gray-400; - } - - /* 链接样式 */ - a { - @apply text-[#00684a] hover:text-[#005a3f] transition-colors duration-200; - } - - /* 主要文本颜色 */ - .text-primary { - @apply text-[#00684a]; - } - - .bg-primary { - @apply bg-[#00684a]; - } - - /* 平滑过渡 */ - .transition-all-ease { - @apply transition-all duration-200 ease-in-out; - } -} \ No newline at end of file diff --git a/app/styles/components/index.css b/app/styles/components/index.css deleted file mode 100644 index 3d21be8..0000000 --- a/app/styles/components/index.css +++ /dev/null @@ -1,22 +0,0 @@ -/** - * 组件样式索引文件 - * 集中导入所有组件样式 - */ - -/* 布局相关组件 */ -@import "../layout.css"; -/* 已合并到layout.css中 */ -/* @import "./sidebar.css"; */ - -/* UI 基础组件 */ -@import "./button.css"; -@import "./card.css"; -@import "./form.css"; -@import "./table.css"; -@import "./badge.css"; -@import "./navigation.css"; - -/** - * 注意:如果上述导入的文件不存在,将会在构建时报错 - * 请确保先创建这些文件,或者先注释掉不存在的文件 - */ \ No newline at end of file diff --git a/app/styles/index.css b/app/styles/index.css deleted file mode 100644 index a59b62b..0000000 --- a/app/styles/index.css +++ /dev/null @@ -1,19 +0,0 @@ -/** - * 主样式入口文件 - * 本文件集中导入所有样式 - */ - -/* Tailwind 基础指令 */ -@import "./base.css"; - -/* 组件样式 */ -@import "./components/index.css"; - -/* 页面特定样式 */ -@import "./pages/index.css"; - -/* - * 注意: 页面特定样式已经移到各自页面的links函数中按需加载 - * 不再通过全局样式文件导入 - */ -/* @import "./pages/index.css"; */ \ No newline at end of file diff --git a/app/styles/layout.css b/app/styles/layout.css deleted file mode 100644 index edea2a8..0000000 --- a/app/styles/layout.css +++ /dev/null @@ -1,201 +0,0 @@ -/** - * 全局布局样式 - * 定义应用的主要布局结构 - */ - -/** - * 侧边栏基础样式 - */ -.sidebar { - @apply w-[280px] h-screen bg-white border-r border-gray-100 flex flex-col - transition-all duration-300 fixed left-0 top-0 z-[100] overflow-y-auto - shadow-[0_0_15px_rgba(0,0,0,0.05)]; -} - -.sidebar.collapsed { - @apply w-20; -} - -/* 侧边栏头部 */ -.sidebar-header { - @apply h-16 border-b border-gray-100 flex items-center justify-between px-5; -} - -.sidebar-logo { - @apply flex items-center space-x-2; -} - -.sidebar-logo-img { - @apply h-8 w-auto; -} - -.sidebar-logo-text { - @apply font-medium text-lg text-gray-900 transition-opacity duration-200; -} - -.sidebar.collapsed .sidebar-logo-text { - @apply opacity-0 invisible; -} - -.sidebar-toggle { - @apply bg-transparent border-none text-xl text-gray-500 cursor-pointer p-1 - rounded transition-all duration-200; -} - -.sidebar-toggle:hover { - @apply bg-gray-100; -} - -/* 用户资料 */ -.sidebar-user { - @apply flex items-center px-5 py-3 border-b border-gray-100; -} - -.sidebar-user-avatar { - @apply w-10 h-10 rounded-full overflow-hidden flex-shrink-0; -} - -.sidebar-user-info { - @apply ml-3 transition-opacity duration-200; -} - -.sidebar.collapsed .sidebar-user-info { - @apply opacity-0 invisible; -} - -.sidebar-user-name { - @apply text-sm font-medium text-gray-900; -} - -.sidebar-user-role { - @apply text-xs text-gray-500; -} - -/* 导航菜单 */ -.sidebar-menu { - @apply flex-1 overflow-y-auto py-4 px-3; -} - -.sidebar-menu-item { - @apply flex items-center py-3 px-4 text-gray-800 no-underline rounded-md - cursor-pointer transition-all duration-200 mb-1; -} - -.sidebar-menu-item i { - @apply text-base w-6 text-center; -} - -.sidebar-menu-icon { - @apply text-xl opacity-80 w-5 flex-shrink-0; -} - -.sidebar-menu-text { - @apply ml-2.5 transition-opacity duration-200; -} - -.sidebar.collapsed .sidebar-menu-text { - @apply opacity-0 invisible; -} - -.sidebar-menu-badge { - @apply ml-auto bg-[#00684a] text-white text-xs font-semibold h-5 min-w-[20px] rounded-full - flex items-center justify-center px-1 transition-opacity duration-200; -} - -.sidebar.collapsed .sidebar-menu-badge { - @apply opacity-0 invisible; -} - -.sidebar-menu-item:hover { - @apply bg-[rgba(0,104,74,0.05)]; -} - -.sidebar-menu-item.active { - @apply bg-[rgba(0,104,74,0.1)] text-[#00684A] font-medium; -} - -.sidebar-submenu { - @apply pl-7 mt-1 space-y-1 transition-all duration-200 overflow-hidden; -} - -.sidebar-submenu-item { - @apply flex items-center px-3 py-2 rounded-md text-sm text-gray-500 - hover:bg-gray-50 transition-colors duration-100; -} - -.sidebar-submenu-item.active { - @apply text-[#00684a]; -} - -/* 侧边栏底部 */ -.sidebar-footer { - @apply border-t border-gray-100 px-5 py-4; -} - -/** - * 主布局容器 - */ -.layout-container { - @apply flex min-h-screen bg-gray-50; -} - -/** - * 主内容区域 - */ -.main-content { - @apply flex-1 ml-[280px] transition-all duration-300 min-h-screen flex flex-col; -} - -.main-content.sidebar-collapsed { - @apply ml-20; -} - -.content-container { - @apply flex-1 p-5 overflow-auto; -} - -/** - * 面包屑导航 - */ -.breadcrumb { - @apply flex items-center text-sm text-gray-500 mb-4; -} - -.breadcrumb-item { - @apply flex items-center; -} - -.breadcrumb-item:not(:last-child)::after { - content: "/"; - @apply mx-2; -} - -.breadcrumb-item:last-child { - @apply text-gray-700 font-medium; -} - -/** - * 响应式调整 - */ -@screen sm { - .content-container { - @apply p-6; - } -} - -@screen md { - .sidebar-toggle { - @apply block; - } -} - -/** - * 暗色模式 - */ -.dark .layout-container { - @apply bg-gray-900; -} - -.dark .content-container { - @apply bg-gray-900 text-gray-200; -} \ No newline at end of file diff --git a/app/styles/main.css b/app/styles/main.css new file mode 100644 index 0000000..78a7a83 --- /dev/null +++ b/app/styles/main.css @@ -0,0 +1,250 @@ +/** + * 主样式文件 + * 包含应用所有样式 + */ + +/* Tailwind 基础指令 */ +@tailwind base; +@tailwind components; +@tailwind utilities; + +/* 全局变量和基础样式 */ +@layer base { + :root { + /* 主题颜色变量 */ + --color-primary: #00684a; + --color-primary-hover: #005a3f; + --color-primary-light: rgba(0, 104, 74, 0.1); + + /* 成功、警告、错误颜色 */ + --color-success: #52c41a; + --color-warning: #faad14; + --color-error: #f5222d; + + /* 中性色 */ + --color-gray-50: #f8f9fa; + --color-gray-100: #f1f3f5; + --color-gray-200: #e9ecef; + --color-gray-300: #dee2e6; + --color-gray-400: #ced4da; + --color-gray-500: #adb5bd; + --color-gray-600: #868e96; + --color-gray-700: #495057; + --color-gray-800: #343a40; + --color-gray-900: #212529; + } + + /* 基本元素样式 */ + html, body { + @apply text-gray-800 antialiased; + font-smooth: always; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + text-rendering: optimizeLegibility; + scroll-behavior: smooth; + } + + /* 文字选择颜色 */ + ::selection { + @apply bg-[rgba(0,104,74,0.2)] text-[#00684a]; + } + + /* 滚动条美化 */ + ::-webkit-scrollbar { + @apply w-2 h-2; + } + + ::-webkit-scrollbar-track { + @apply bg-gray-100; + } + + ::-webkit-scrollbar-thumb { + @apply bg-gray-300 rounded-full hover:bg-gray-400; + } + + /* 链接样式 */ + a { + @apply text-[#00684a] hover:text-[#005a3f] transition-colors duration-200; + } +} + +/* 组件相关样式 */ +@layer components { + /* 文本颜色工具类 */ + .text-primary { + @apply text-[#00684a]; + } + + .bg-primary { + @apply bg-[#00684a]; + } + + .transition-all-ease { + @apply transition-all duration-200 ease-in-out; + } + + /* === 布局组件 === */ + .layout-container { + @apply flex min-h-screen bg-gray-50; + } + + /* 用户资料 */ + .user-profile { + @apply p-4 border-b border-gray-100 flex items-center; + } + + .avatar { + @apply w-10 h-10 rounded-full bg-primary text-white flex items-center justify-center; + } + + /* === 侧边栏 === */ + .sidebar { + @apply w-[280px] h-screen bg-white border-r border-gray-100 flex flex-col + transition-all duration-300 fixed left-0 top-0 z-[100] overflow-y-auto + shadow-[0_0_15px_rgba(0,0,0,0.05)]; + } + + .sidebar.collapsed { + @apply w-20; + } + + .sidebar-menu { + @apply flex-1 overflow-y-auto py-4 px-3; + } + + .sidebar-menu-item { + @apply flex items-center py-3 px-4 text-gray-800 no-underline rounded-md + cursor-pointer transition-all duration-200 mb-1 relative + hover:bg-[rgba(0,104,74,0.05)]; + /* 确保点击事件可以正常工作 */ + pointer-events: auto; + z-index: 10; + } + + .sidebar-menu-item i { + @apply text-base w-6 text-center; + } + + .sidebar-menu-text { + @apply ml-2.5 transition-opacity duration-200; + } + + .sidebar.collapsed .sidebar-menu-text { + @apply opacity-0 invisible; + } + + .sidebar-menu-item.active { + @apply bg-[rgba(0,104,74,0.1)] text-[#00684A] font-medium; + } + + .submenu-container { + @apply mt-1 mb-2 space-y-1 overflow-hidden border-l border-gray-100 ml-4 pl-3 relative; + /* 确保子菜单在父菜单之上 */ + z-index: 5; + } + + .sidebar.collapsed .submenu-container { + @apply border-l-0 pl-0; + } + + .submenu-container .sidebar-menu-item { + @apply py-2 pl-2 text-sm; + } + + .sidebar-menu-item:hover { + @apply bg-[rgba(0,104,74,0.05)]; + } + + .sidebar-toggle { + @apply bg-transparent border-none text-xl text-gray-500 cursor-pointer p-1 + rounded transition-all duration-200; + } + + .sidebar-toggle:hover { + @apply bg-gray-100; + } + + /* === 主内容区域 === */ + .main-content { + @apply flex-1 ml-[280px] transition-all duration-300 min-h-screen flex flex-col; + } + + .main-content.sidebar-collapsed { + @apply ml-20; + } + + .content-container { + @apply flex-1 p-5 overflow-auto; + } + + /* === 面包屑导航 === */ + .breadcrumb { + @apply flex items-center text-sm text-gray-500 mb-4; + } + + .breadcrumb-item { + @apply flex items-center; + } + + .breadcrumb-item:not(:last-child)::after { + content: "/"; + @apply mx-2; + } + + .breadcrumb-item:last-child { + @apply text-gray-700 font-medium; + } + + /* === 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; + } + + /* === 响应式调整 === */ + @screen sm { + .content-container { + @apply p-6; + } + } + + @screen md { + .sidebar-toggle { + @apply block; + } + } + + /* === 暗色模式 === */ + .dark .layout-container { + @apply bg-gray-900; + } + + .dark .content-container { + @apply bg-gray-900 text-gray-200; + } +} \ No newline at end of file diff --git a/app/styles/pages/index.css b/app/styles/pages/index.css deleted file mode 100644 index b165035..0000000 --- a/app/styles/pages/index.css +++ /dev/null @@ -1,26 +0,0 @@ -/** - * 页面样式索引文件 - * 集中导入所有页面特定样式 - */ - -/* 首页 */ -@import "./home.css"; - -/* 评查点列表 */ -@import "./rules_index.css"; - -/* 评查点分组 */ -@import "./rule-groups.css"; - -/* 其他页面 - 待创建 */ -/* -@import "./files.css"; -@import "./rules.css"; -@import "./reviews.css"; -@import "./settings.css"; -*/ - -/** - * 注意:如果上述导入的文件不存在,将会在构建时报错 - * 请确保先创建这些文件,或者先注释掉不存在的文件 - */ \ No newline at end of file diff --git a/app/styles/pages/rule-groups.css b/app/styles/pages/rule-groups.css deleted file mode 100644 index 9dc9026..0000000 --- a/app/styles/pages/rule-groups.css +++ /dev/null @@ -1,76 +0,0 @@ -/* 评查点分组管理页面样式 */ - -/* 树形结构样式 */ -.tree-table .group-row { - transition: all 0.2s; -} - -.tree-table .group-row:hover { - background-color: rgba(0, 104, 74, 0.05); -} - -.tree-table .parent-row { - background-color: #f9f9f9; - font-weight: 500; -} - -.tree-table .child-row { - border-left: 3px solid #f0f0f0; -} - -.expand-icon { - width: 22px; - height: 22px; - border-radius: 4px; - display: inline-flex; - align-items: center; - justify-content: center; - cursor: pointer; - transition: all 0.2s; -} - -.expand-icon:hover { - background-color: rgba(0, 104, 74, 0.1); -} - -.group-badge { - display: inline-flex; - align-items: center; - padding: 2px 8px; - border-radius: 12px; - font-size: 12px; - margin-left: 8px; -} - -.parent-badge { - background-color: rgba(0, 104, 74, 0.1); - color: var(--color-primary); -} - -.child-badge { - background-color: rgba(0, 104, 74, 0.05); - color: var(--color-primary); -} - -.badge { - display: inline-flex; - align-items: center; - justify-content: center; - padding: 2px 8px; - border-radius: 12px; - font-size: 12px; -} - -/* 按钮文本样式,不与其他冲突 */ -.btn-text { - background-color: transparent; - border: none; - color: var(--color-gray-800); - padding: 4px 8px; -} - -/* 小尺寸按钮 */ -.btn-sm { - padding: 4px 8px; - font-size: 12px; -} \ No newline at end of file diff --git a/app/styles/pages/rule-groups_index.css b/app/styles/pages/rule-groups_index.css new file mode 100644 index 0000000..4a0dd05 --- /dev/null +++ b/app/styles/pages/rule-groups_index.css @@ -0,0 +1,319 @@ +/* app/styles/pages/rule-groups_index.css */ +/* 使用命名空间限制样式作用范围,避免覆盖全局样式 */ +.rule-groups-page a.badge.bg-primary.text-white { + color: white; + text-decoration: none; + transition: color 0.2s; +} + +.rule-groups-page a.badge.bg-primary.text-white:hover { + color: white; + opacity: 0.9; + text-decoration: underline; +} + +/* 修改分组名称链接颜色为绿色 */ +.rule-groups-page .tree-table a.text-primary { + color: #00684a; + text-decoration: none; +} + +.rule-groups-page .tree-table a.text-primary:hover { + text-decoration: underline; + color: #005a3f; +} + +/* 分组名称链接样式 */ +.rule-groups-page .group-name-link { + color: #00684a; + text-decoration: none; + transition: all 0.2s; +} + +.rule-groups-page .group-name-link:hover { + text-decoration: underline; + color: #005a3f; +} + +/* 展开/收起图标颜色 */ +.rule-groups-page .expand-icon i { + color: #00684a; +} + +/* 树形结构样式 */ +.rule-groups-page .tree-table .group-row { + transition: all 0.2s; + font-weight: 400; /* 减少文字粗细度 */ + } + + .rule-groups-page .tree-table .group-row:hover { + background-color: rgba(0, 104, 74, 0.05); + } + + .rule-groups-page .tree-table .parent-row { + background-color: #f9f9f9; + font-weight: 400; /* 减少文字粗细度 */ + } + + .rule-groups-page .tree-table .child-row { + border-left: 3px solid #f0f0f0; + } + + .rule-groups-page .expand-icon { + width: 22px; + height: 22px; + border-radius: 4px; + display: inline-flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all 0.2s; + } + + .rule-groups-page .expand-icon:hover { + background-color: rgba(0, 104, 74, 0.1); + } + + .rule-groups-page .group-badge { + display: inline-flex; + align-items: center; + padding: 2px 8px; + border-radius: 12px; + font-size: 12px; + margin-left: 8px; + } + + .rule-groups-page .parent-badge { + background-color: rgba(0, 104, 74, 0.1); + color: var(--color-primary); + } + + .rule-groups-page .child-badge { + background-color: rgba(0, 104, 74, 0.05); + 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; + margin-bottom: 8px; + color: #495057; + font-size: 14px; + } + + .rule-groups-page .form-input, + .rule-groups-page .form-select { + width: 100%; + height: 38px; + padding: 8px 12px; + border: 1px solid #dee2e6; + border-radius: 4px; + font-size: 14px; + transition: all 0.2s; + } + + .rule-groups-page .form-input:focus, + .rule-groups-page .form-select:focus { + border-color: #00684a; + box-shadow: 0 0 0 2px rgba(0, 104, 74, 0.2); + 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; + align-items: center; + justify-content: center; + padding: 0.25rem 0.5rem; + border-radius: 0.75rem; + font-size: 0.75rem; + font-weight: 400; /* 减少文字粗细度 */ + } + + /* 添加badge链接的特殊悬停样式 */ + .rule-groups-page a.badge.bg-primary.text-white:hover { + color: white; + opacity: 0.9; + } + + /* 仅在rule-groups页面内生效的样式 */ + .rule-groups-page .bg-primary { + background-color: #00684a; + } + + .rule-groups-page .text-white { + color: white; + } + + .rule-groups-page .text-secondary { + color: #6c757d; + } + + .rule-groups-page .text-error { + color: #f5222d; + } + + /* 行操作按钮 */ + .rule-groups-page .ant-btn-text { + background: transparent; + margin: 10px; + border: none; + padding: 4px 0; + cursor: pointer; + transition: color 0.2s; + } + + .rule-groups-page .ant-btn-text.text-primary { + color: #00684a; + } + + .rule-groups-page .ant-btn-text.text-primary:hover { + color: #005a3f; + } + + .rule-groups-page .ant-btn-text.text-error { + color: #f5222d; + } + + .rule-groups-page .ant-btn-text.text-error:hover { + color: #cf1f29; + } + + /* 响应式调整 */ + @media (max-width: 768px) { + .rule-groups-page .flex-wrap { + flex-direction: column; + } + + .rule-groups-page .flex-1 { + width: 100%; + } + + .rule-groups-page .ant-table { + font-size: 14px; + } + } + + /* 搜索框样式 */ + .rule-groups-page .search-box { + display: flex; + align-items: center; + } + + .rule-groups-page .search-box .form-input { + flex: 1; + border-top-right-radius: 0; + border-bottom-right-radius: 0; + } + + .rule-groups-page .search-box .ant-btn { + border-top-left-radius: 0; + border-bottom-left-radius: 0; + } + + /* 无按钮搜索框样式 */ + .rule-groups-page .search-box.form-input-only .form-input { + border-radius: 0.25rem; + width: 100%; + } + diff --git a/app/styles/pages/rules_files.css b/app/styles/pages/rules_files.css new file mode 100644 index 0000000..639ec24 --- /dev/null +++ b/app/styles/pages/rules_files.css @@ -0,0 +1,240 @@ +/* 评查文件列表页面样式 */ +.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; +} \ No newline at end of file diff --git a/app/styles/pages/rules_index.css b/app/styles/pages/rules_index.css index 8b0ae32..85e81ff 100644 --- a/app/styles/pages/rules_index.css +++ b/app/styles/pages/rules_index.css @@ -1,40 +1,120 @@ /* 评查点列表页面样式 */ +.rules-page { + /* 所有样式都包含在此命名空间内 */ +} /* 筛选区域 */ -.filter-card { +.rules-page .filter-card { margin-bottom: 1rem; } /* 搜索框样式 */ -.search-box { +.rules-page .search-box { display: flex; align-items: center; } -.search-box .form-input { +.rules-page .search-box .form-input { flex: 1; border-top-right-radius: 0; border-bottom-right-radius: 0; } -.search-box .ant-btn { +.rules-page .search-box .ant-btn { border-top-left-radius: 0; border-bottom-left-radius: 0; } -/* 表格样式 */ -.rules-table { +/* 无按钮搜索框样式 */ +.rules-page .search-box.form-input-only .form-input { + border-radius: 0.25rem; width: 100%; - margin-top: 1rem; +} + +/* 表格样式 */ +.rules-page .ant-table { + width: 100%; + border-collapse: collapse; +} + +.rules-page .ant-table th { + background-color: #f9f9f9; + font-weight: 400; /* 减少文字粗细度 */ + padding: 12px 16px; + text-align: left; + border-bottom: 1px solid #e9ecef; +} + +.rules-page .ant-table td { + padding: 12px 16px; + border-bottom: 1px solid #e9ecef; + font-weight: 400; /* 减少文字粗细度 */ +} + +.rules-page .ant-table tr:hover { + background-color: rgba(0, 0, 0, 0.02); } /* 表格操作列样式 */ -.operations-cell { +.rules-page .operations-cell { white-space: nowrap; } +/* 操作按钮样式 - 改为文本按钮样式 */ +.rules-page .operations-cell .ant-btn { + background: transparent; + border: 1px solid #e9ecef; + padding: 4px 8px; + margin-right: 4px; + border-radius: 4px; + font-size: 14px; + color: #495057; + height: auto; + min-width: auto; + box-shadow: none; +} + +.rules-page .operation-btn { + margin-right: 8px; + font-weight: normal; + display: inline-flex; + align-items: center; + justify-content: center; + border: 1px solid #e4e4e4; + background-color: #ffffff; + color: #333; + border-radius: 4px; + line-height: 1; + padding: 4px 8px; + font-size: 13px; + text-decoration: none; + cursor: pointer; +} + +.rules-page .operation-btn i { + font-size: 14px; + margin-right: 2px; +} + +.rules-page .operation-btn:hover { + border-color: #00684a; + color: #00684a; +} + +.rules-page .operation-btn-danger { + background-color: #f5222d; + border-color: #f5222d; + color: white; +} + +.rules-page .operation-btn-danger:hover { + background-color: #cf1f29; + border-color: #cf1f29; + color: white !important; +} + /* 状态点样式 */ -.status-dot { +.rules-page .status-dot { display: inline-block; width: 8px; height: 8px; @@ -42,28 +122,177 @@ margin-right: 5px; } -.status-dot-success { +.rules-page .status-dot-success { background-color: #52c41a; } -.status-dot-default { +.rules-page .status-dot-default { background-color: #d9d9d9; } /* 标签自定义样式 */ -.rule-tag { +.rules-page .ant-tag { + display: inline-flex; + align-items: center; padding: 2px 8px; font-size: 12px; border-radius: 4px; + margin-right: 8px; + font-weight: 400; /* 减少文字粗细度 */ } -/* 表格行悬停效果 */ -.ant-table tbody tr:hover { - background-color: rgba(0, 0, 0, 0.02); +.rules-page .ant-tag-blue { + background-color: rgba(24, 144, 255, 0.1); + color: #1890ff; +} + +.rules-page .ant-tag-green { + background-color: rgba(82, 196, 26, 0.1); + color: #52c41a; +} + +.rules-page .ant-tag-cyan { + background-color: rgba(19, 194, 194, 0.1); + color: #13c2c2; +} + +.rules-page .ant-tag-purple { + background-color: rgba(114, 46, 209, 0.1); + color: #722ed1; +} + +.rules-page .ant-tag-orange { + background-color: rgba(250, 173, 20, 0.1); + color: #faad14; +} + +.rules-page .ant-tag-red { + background-color: rgba(245, 34, 45, 0.1); + color: #f5222d; +} + +/* 分页样式 */ +.rules-page .ant-pagination { + display: flex; + align-items: center; + margin-top: 16px; + padding: 16px; + justify-content: space-between; +} + +.rules-page .ant-pagination-right { + display: flex; + align-items: center; + gap: 4px; +} + +.rules-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; +} + +.rules-page .ant-pagination-item-active { + background-color: #00684a; + border-color: #00684a; + color: white; +} + +.rules-page .ant-pagination-disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.rules-page .ant-pagination-prev, +.rules-page .ant-pagination-next { + min-width: 32px; + height: 32px; +} + +.rules-page .ant-pagination-options { + display: flex; + align-items: center; +} + +.rules-page .ant-pagination-options-size-changer { + margin-left: 8px; + border: 1px solid #dee2e6; + border-radius: 4px; + height: 32px; + padding: 0 8px; +} + +/* 卡片内容调整 */ +.rules-page .content-card .ant-card-body { + padding: 0 !important; +} + +/* 表单标签 */ +.rules-page .form-label { + display: block; + margin-bottom: 8px; + font-size: 14px; + color: #495057; +} + +.rules-page .form-select, +.rules-page .form-input { + width: 100%; + height: 38px; + padding: 8px 12px; + border: 1px solid #dee2e6; + border-radius: 4px; + font-size: 14px; + transition: all 0.2s; +} + +.rules-page .form-select:focus, +.rules-page .form-input:focus { + border-color: #00684a; + box-shadow: 0 0 0 2px rgba(0, 104, 74, 0.2); + outline: none; +} + +/* 确认对话框 */ +.rules-page .modal-backdrop { + position: fixed; + inset: 0; + background-color: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; +} + +.rules-page .modal-content { + background-color: white; + border-radius: 8px; + padding: 24px; + max-width: 400px; + width: 100%; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); +} + +.rules-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; } /* 页面标题区域 */ -.page-header { +.rules-page .page-header { display: flex; justify-content: space-between; align-items: center; @@ -71,10 +300,44 @@ } /* 卡片内容间距 */ -.content-card { - padding: 0; +.rules-page .card-container { + margin-bottom: 16px; } -.content-card .ant-card-body { - padding: 0; -} \ No newline at end of file +.rules-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; +} + +.rules-page .ant-card-body { + padding: 16px; +} + +/* 按钮样式修正 */ +.rules-page .ant-btn-sm { + height: 32px; + padding: 0 8px; + font-size: 14px; +} + +/* 按钮悬停覆盖样式 */ +.rules-page .ant-btn-primary { + color: white !important; +} + +.rules-page .ant-btn-primary:hover { + color: white !important; + background-color: #005a3f; +} + +/* 针对操作列中的按钮 +.rules-page .operations-cell .ant-btn-default:hover { + color: #495057; +} + +.rules-page .operations-cell .ant-btn-danger:hover { + color: white; +} */ \ No newline at end of file diff --git a/app/styles/tailwind.css b/app/styles/tailwind.css deleted file mode 100644 index 416da3f..0000000 --- a/app/styles/tailwind.css +++ /dev/null @@ -1,342 +0,0 @@ -/** - * Tailwind CSS 配置 - * - * 这个文件曾经包含所有样式定义,但现在已重组为更模块化的结构。 - * 请使用 app/styles/index.css 作为主样式入口文件。 - * 以下样式已转移到其他组件特定的CSS文件中,保留在此仅作备份参考。 - */ - -@import "./base.css"; - -:root { - --primary-color: #00684a; - --primary-hover: #005a40; - --primary-light: rgba(0, 104, 74, 0.1); - --success-color: #52c41a; - --warning-color: #faad14; - --error-color: #ff4d4f; - --text-color: rgba(0, 0, 0, 0.85); - --text-secondary: rgba(0, 0, 0, 0.45); - --border-color: #f0f0f0; - --bg-gray: #f5f5f5; -} - -/* -@layer components { - /* 布局容器 */ - /* - .layout-container { - @apply flex h-screen w-full overflow-hidden; - } - - /* 侧边栏 */ - /* - .sidebar { - @apply w-60 h-screen bg-blue-50 border-r border-gray-100 flex flex-col transition-all duration-300 fixed left-0 top-0 z-50 overflow-y-auto shadow-sm; - } - - .sidebar.collapsed { - @apply w-16; - } - - /* 主内容区 */ - /* - .main-content { - @apply flex-1 p-5 overflow-y-auto ml-60 transition-all duration-300; - } - - .main-content.sidebar-collapsed { - @apply ml-16; - } - - /* 面包屑导航 */ - /* - .breadcrumb { - @apply flex items-center text-sm text-gray-500 mb-4; - } - - .breadcrumb-item { - @apply flex items-center; - } - - .breadcrumb-item:not(:last-child)::after { - content: "/"; - @apply mx-2 text-gray-400; - } - - .breadcrumb-item:last-child { - @apply text-gray-700 font-medium; - } - - /* 侧边栏菜单 */ - /* - .sidebar-menu-item { - @apply py-2 px-4 hover:bg-gray-50 rounded-md transition-all duration-200 mb-1; - } - - .sidebar-menu-item.active { - @apply bg-primary bg-opacity-10; - } - - .sidebar-menu-item.active a { - @apply text-primary font-medium; - } - - /* 卡片样式 */ - /* - .dashboard-card { - @apply bg-white rounded-lg shadow-sm p-5 mb-5; - } - - .card-title { - @apply text-base font-semibold mb-4 flex items-center text-gray-800; - } - - .card-title i { - @apply text-xl mr-2 text-primary; - } - - /* 统计卡片网格 */ - /* - .stat-grid { - @apply grid grid-cols-1 md:grid-cols-4 gap-4; - } - - /* 状态标签 */ - /* - .status-badge { - @apply inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium; - } - - .status-badge.status-success { - @apply bg-green-50 text-green-600; - } - - .status-badge.status-warning { - @apply bg-yellow-50 text-yellow-600; - } - - .status-badge.status-error { - @apply bg-red-50 text-red-600; - } - - /* 状态点 */ - /* - .status-dot { - @apply inline-block w-2 h-2 rounded-full mr-1.5; - } - - .status-dot-success { - @apply bg-green-500; - } - - .status-dot-warning { - @apply bg-yellow-500; - } - - .status-dot-error { - @apply bg-red-500; - } - - .status-dot-default { - @apply bg-gray-400; - } - - /* 表单组件 */ - /* - .form-label { - @apply block text-sm font-medium text-gray-700 mb-1; - } - - .form-input { - @apply block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-primary focus:border-primary sm:text-sm; - } - - .form-select { - @apply block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-primary focus:border-primary sm:text-sm; - } - - .search-box { - @apply relative flex; - } - - .search-box input { - @apply pr-10; - } - - .search-box button { - @apply absolute inset-y-0 right-0 flex items-center px-2; - } - - /* 表格样式 */ - /* - .ant-table { - @apply w-full bg-white rounded-md overflow-hidden; - } - - .ant-table th { - @apply py-3 px-4 text-left text-sm font-medium text-gray-600 bg-gray-50 border-b border-gray-200; - } - - .ant-table td { - @apply py-3 px-4 text-sm text-gray-700 border-b border-gray-100; - } - - .ant-table tr:hover td { - @apply bg-gray-50; - } - - /* 按钮样式 */ - /* - .ant-btn { - @apply inline-flex items-center justify-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium focus:outline-none transition-colors duration-200; - } - - .ant-btn-primary { - @apply bg-primary text-white hover:bg-primary-dark; - } - - .ant-btn-default { - @apply bg-white text-gray-700 border-gray-300 hover:bg-gray-50; - } - - .ant-btn-danger { - @apply bg-white text-red-600 border-gray-300 hover:bg-red-50 hover:border-red-300; - } - - .ant-btn-sm { - @apply px-2.5 py-1 text-xs; - } - - /* 标签样式 */ - /* - .ant-tag { - @apply inline-flex items-center px-2 py-0.5 rounded-md text-xs font-medium; - } - - .ant-tag-blue { - @apply bg-blue-50 text-blue-600; - } - - .ant-tag-green { - @apply bg-green-50 text-green-600; - } - - .ant-tag-cyan { - @apply bg-cyan-50 text-cyan-600; - } - - .ant-tag-purple { - @apply bg-purple-50 text-purple-600; - } - - .ant-tag-orange { - @apply bg-orange-50 text-orange-600; - } - - .ant-tag-red { - @apply bg-red-50 text-red-600; - } - - /* 卡片样式 */ - /* - .ant-card { - @apply bg-white rounded-md shadow-sm overflow-hidden mb-5; - } - - .ant-card-body { - @apply p-5; - } - - /* 评查结果面板 */ - /* - .review-points-panel { - @apply bg-white rounded-md shadow-sm overflow-hidden h-full; - } - - .review-panel-header { - @apply bg-primary bg-opacity-10 flex items-center; - } - - /* 评查点样式 */ - /* - .review-point-item { - @apply border-b border-gray-100 p-3 hover:bg-gray-50 cursor-pointer; - } - - .review-point-header { - @apply flex items-center justify-between mb-1; - } - - .review-point-title { - @apply text-sm font-medium text-gray-700; - } - - .review-point-location { - @apply flex items-center text-xs text-gray-500; - } - - /* 选项卡 */ - /* - .tab-container { - @apply bg-white rounded-md shadow-sm overflow-hidden; - } - - .tab-nav { - @apply flex border-b border-gray-200; - } - - .tab-nav-item { - @apply flex items-center py-3 px-4 text-sm font-medium text-gray-600 cursor-pointer; - } - - .tab-nav-item i { - @apply mr-1.5; - } - - .tab-nav-item.active { - @apply text-primary border-b-2 border-primary; - } - - .tab-content { - @apply p-4; - } - - .tab-pane { - @apply hidden; - } - - .tab-pane.active { - @apply block; - } - - /* 辅助类 */ - /* - .text-primary { - @apply text-green-700; - } - - .bg-primary { - @apply bg-green-700; - } - - .bg-primary-light { - @apply bg-green-50; - } - - .text-success { - @apply text-green-600; - } - - .text-warning { - @apply text-yellow-600; - } - - .text-error { - @apply text-red-600; - } - - .border-primary { - @apply border-green-700; - } -} */ \ No newline at end of file diff --git a/html/配置-列表.html b/html/配置-列表.html index 6902f2c..9cc0fa3 100644 --- a/html/配置-列表.html +++ b/html/配置-列表.html @@ -10,7 +10,7 @@ --> - +